From 7a2a17557cd24c14eb1f7bbfcc6d228bb593da50 Mon Sep 17 00:00:00 2001
From: Matia <38083522+matiadev@users.noreply.github.com>
Date: Mon, 6 Apr 2026 16:24:56 +0200
Subject: [PATCH 1/7] docs: add CSS custom properties section to style
directive (#18065)
Adds documentation for using CSS custom properties with the `style:`
directive. I think this was lost at some point during the great
transition. It's only mentioned in the [best
practices](https://svelte.dev/docs/svelte/best-practices#Using-JavaScript-variables-in-CSS)
section, so I thought it would make sense to include it.
---
documentation/docs/03-template-syntax/17-style.md | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/documentation/docs/03-template-syntax/17-style.md b/documentation/docs/03-template-syntax/17-style.md
index 8b25c221d6..6ddb128f4a 100644
--- a/documentation/docs/03-template-syntax/17-style.md
+++ b/documentation/docs/03-template-syntax/17-style.md
@@ -42,3 +42,9 @@ even over `!important` properties:
This will be red
This will still be red
```
+
+You can set CSS custom properties:
+
+```svelte
+
...
+```
From 14adb8caa9eb44b3b4c38233e00740b01bb65e7e Mon Sep 17 00:00:00 2001
From: ottomated <31470743+ottomated@users.noreply.github.com>
Date: Mon, 6 Apr 2026 07:34:33 -0700
Subject: [PATCH 2/7] fix: correct types for `ontoggle` on elements
(#18063)
`` elements fire `ontoggle` as ToggleEvents
([source](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/toggle_event)),
but they're currently just typed as Event.
### Before submitting the PR, please make sure you do the following
- [ ] It's really useful if your PR references an issue where it is
discussed ahead of time. In many cases, features are absent for a
reason. For large changes, please create an RFC:
https://github.com/sveltejs/rfcs
- [x] Prefix your PR title with `feat:`, `fix:`, `chore:`, or `docs:`.
- [ ] This message body should clearly illustrate what problems it
solves.
- [ ] Ideally, include a test that fails without this PR but passes with
it.
- [x] If this PR changes code within `packages/svelte/src`, add a
changeset (`npx changeset`).
### Tests and linting
- [x] Run the tests with `pnpm test` and lint the project with `pnpm
lint`
---
.changeset/great-toes-behave.md | 5 +++++
packages/svelte/elements.d.ts | 6 +++---
2 files changed, 8 insertions(+), 3 deletions(-)
create mode 100644 .changeset/great-toes-behave.md
diff --git a/.changeset/great-toes-behave.md b/.changeset/great-toes-behave.md
new file mode 100644
index 0000000000..26e36f70f1
--- /dev/null
+++ b/.changeset/great-toes-behave.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+fix: correct types for `ontoggle` on elements
diff --git a/packages/svelte/elements.d.ts b/packages/svelte/elements.d.ts
index 885004dd2a..f18b7dea98 100644
--- a/packages/svelte/elements.d.ts
+++ b/packages/svelte/elements.d.ts
@@ -952,9 +952,9 @@ export interface HTMLDetailsAttributes extends HTMLAttributes | undefined | null;
- ontoggle?: EventHandler | undefined | null;
- ontogglecapture?: EventHandler | undefined | null;
+ 'on:toggle'?: ToggleEventHandler | undefined | null;
+ ontoggle?: ToggleEventHandler | undefined | null;
+ ontogglecapture?: ToggleEventHandler | undefined | null;
}
export interface HTMLDelAttributes extends HTMLAttributes {
From adba758067e0e68f9fbdcd72a6103437ec061a27 Mon Sep 17 00:00:00 2001
From: Simon H <5968653+dummdidumm@users.noreply.github.com>
Date: Mon, 6 Apr 2026 16:52:29 +0200
Subject: [PATCH 3/7] fix: don't reset status of uninitialized deriveds
(#18054)
If a new branch is created (e.g. if block becomes truthy) inside a fork
and a derived is read inside the new branch for the first time, it will
get reactions but its true value will stay uninitialized due to the
fork. If the fork then commits, it will NOT change its value because it
will not reexecute: there is no effect running after commit telling it
to do that, because it was only read inside new effects which already
ran while still inside the fork.
Now, when that branch is removed (e.g. if block becomes falsy), the
derived loses its reactions again and becomes disconnected, at which
point the status is reset. So we end up with a maybe_dirty or clean
derived and an uninitialized value, causing breakage if it's called
again afterwards.
The fix is to not reset the status in this case.
Fixes https://github.com/sveltejs/kit/issues/15126
Fixes https://github.com/sveltejs/kit/issues/15318
Fixes https://github.com/sveltejs/kit/issues/15061
---
.changeset/silent-rings-yell.md | 5 +++++
.../svelte/src/internal/client/runtime.js | 9 ++++++++-
.../samples/async-fork-if-derived/_config.js | 20 +++++++++++++++++++
.../samples/async-fork-if-derived/main.svelte | 13 ++++++++++++
4 files changed, 46 insertions(+), 1 deletion(-)
create mode 100644 .changeset/silent-rings-yell.md
create mode 100644 packages/svelte/tests/runtime-runes/samples/async-fork-if-derived/_config.js
create mode 100644 packages/svelte/tests/runtime-runes/samples/async-fork-if-derived/main.svelte
diff --git a/.changeset/silent-rings-yell.md b/.changeset/silent-rings-yell.md
new file mode 100644
index 0000000000..0b417103f7
--- /dev/null
+++ b/.changeset/silent-rings-yell.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+fix: don't reset status of uninitialized deriveds
diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js
index 906d68fbf0..d9578142eb 100644
--- a/packages/svelte/src/internal/client/runtime.js
+++ b/packages/svelte/src/internal/client/runtime.js
@@ -399,7 +399,14 @@ function remove_reaction(signal, dependency) {
derived.f &= ~WAS_MARKED;
}
- update_derived_status(derived);
+ // In a fork it's possible that a derived is executed and gets reactions, then commits, but is
+ // never re-executed. This is possible when the derived is only executed once in the context
+ // of a new branch which happens before fork.commit() runs. In this case, the derived still has
+ // UNINITIALIZED as its value, and then when it's loosing its reactions we need to ensure it stays
+ // DIRTY so it is reexecuted once someone wants its value again.
+ if (derived.v !== UNINITIALIZED) {
+ update_derived_status(derived);
+ }
// freeze any effects inside this derived
freeze_derived_effects(derived);
diff --git a/packages/svelte/tests/runtime-runes/samples/async-fork-if-derived/_config.js b/packages/svelte/tests/runtime-runes/samples/async-fork-if-derived/_config.js
new file mode 100644
index 0000000000..4a03a54e7a
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/async-fork-if-derived/_config.js
@@ -0,0 +1,20 @@
+import { tick } from 'svelte';
+import { test } from '../../test';
+
+export default test({
+ async test({ assert, target }) {
+ const [, toggle] = target.querySelectorAll('button');
+
+ toggle?.click();
+ await tick();
+ assert.htmlEqual(target.innerHTML, ` 0`);
+
+ toggle?.click();
+ await tick();
+ assert.htmlEqual(target.innerHTML, ` `);
+
+ toggle?.click();
+ await tick();
+ assert.htmlEqual(target.innerHTML, ` 0`);
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/async-fork-if-derived/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-fork-if-derived/main.svelte
new file mode 100644
index 0000000000..519857a9ce
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/async-fork-if-derived/main.svelte
@@ -0,0 +1,13 @@
+
+
+
+
+
+{#if show}
+ {d_count}
+{/if}
From d86cb5cc327a29e7e509ada93a3338d2118aab93 Mon Sep 17 00:00:00 2001
From: Simon H <5968653+dummdidumm@users.noreply.github.com>
Date: Mon, 6 Apr 2026 16:59:37 +0200
Subject: [PATCH 4/7] fix: skip rebase logic in non-async mode (#18040)
Fixes part of #17940 (the hydration->error thing still needs a repro).
Essentially in sync mode render effects are executed during traversing
the effect tree, and when flushSync is called during that it can cause
the timing of things getting out of sync such that you end up wanting to
rebase another branch, which is never needed in sync mode.
So we just skip that logic in sync mode. Another way would be to adjust
flushSync such that it does appends itself to the end of the current
flushing cycle if it notices processing is already ongoing. This will
not work when you pass a callback function to `flushSync` though -
another indicator that we should probably remove that callback argument.
---------
Co-authored-by: Rich Harris
Co-authored-by: Rich Harris
---
.changeset/ripe-mails-wave.md | 5 +++++
.../src/internal/client/reactivity/batch.js | 5 ++++-
.../flush-sync-each-block/Inner.svelte | 8 ++++++++
.../samples/flush-sync-each-block/_config.js | 20 +++++++++++++++++++
.../samples/flush-sync-each-block/main.svelte | 12 +++++++++++
5 files changed, 49 insertions(+), 1 deletion(-)
create mode 100644 .changeset/ripe-mails-wave.md
create mode 100644 packages/svelte/tests/runtime-legacy/samples/flush-sync-each-block/Inner.svelte
create mode 100644 packages/svelte/tests/runtime-legacy/samples/flush-sync-each-block/_config.js
create mode 100644 packages/svelte/tests/runtime-legacy/samples/flush-sync-each-block/main.svelte
diff --git a/.changeset/ripe-mails-wave.md b/.changeset/ripe-mails-wave.md
new file mode 100644
index 0000000000..6ccc1c724d
--- /dev/null
+++ b/.changeset/ripe-mails-wave.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+fix: skip rebase logic in non-async mode
diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js
index 23cc73ffc8..f98436e7bc 100644
--- a/packages/svelte/src/internal/client/reactivity/batch.js
+++ b/packages/svelte/src/internal/client/reactivity/batch.js
@@ -349,7 +349,9 @@ export class Batch {
next_batch.#process();
}
- if (!batches.has(this)) {
+ // In sync mode flushSync can cause #commit to wrongfully think that there needs to be a rebase, so we only do it in async mode
+ // TODO fix the underlying cause, otherwise this will likely regress when non-async mode is removed
+ if (async_mode_flag && !batches.has(this)) {
this.#commit();
}
}
@@ -791,6 +793,7 @@ export class Batch {
}
}
+// TODO Svelte@6 think about removing the callback argument.
/**
* Synchronously flush any pending updates.
* Returns void if no callback is provided, otherwise returns the result of calling the callback.
diff --git a/packages/svelte/tests/runtime-legacy/samples/flush-sync-each-block/Inner.svelte b/packages/svelte/tests/runtime-legacy/samples/flush-sync-each-block/Inner.svelte
new file mode 100644
index 0000000000..6d7c982db0
--- /dev/null
+++ b/packages/svelte/tests/runtime-legacy/samples/flush-sync-each-block/Inner.svelte
@@ -0,0 +1,8 @@
+
+
+{value}
diff --git a/packages/svelte/tests/runtime-legacy/samples/flush-sync-each-block/_config.js b/packages/svelte/tests/runtime-legacy/samples/flush-sync-each-block/_config.js
new file mode 100644
index 0000000000..2798f533c6
--- /dev/null
+++ b/packages/svelte/tests/runtime-legacy/samples/flush-sync-each-block/_config.js
@@ -0,0 +1,20 @@
+import { tick } from 'svelte';
+import { test } from '../../test';
+
+export default test({
+ async test({ assert, target }) {
+ await tick();
+ const [btn] = target.querySelectorAll('button');
+
+ btn.click();
+ await tick();
+ assert.htmlEqual(
+ target.innerHTML,
+ `
+
+
+ 2
+ `
+ );
+ }
+});
diff --git a/packages/svelte/tests/runtime-legacy/samples/flush-sync-each-block/main.svelte b/packages/svelte/tests/runtime-legacy/samples/flush-sync-each-block/main.svelte
new file mode 100644
index 0000000000..3be1202c97
--- /dev/null
+++ b/packages/svelte/tests/runtime-legacy/samples/flush-sync-each-block/main.svelte
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+{#each [count] as row}
+ {row}
+{/each}
From cd5bda00a81687df415d6b35ad1b16109f847216 Mon Sep 17 00:00:00 2001
From: Rich Harris
Date: Mon, 6 Apr 2026 12:06:41 -0400
Subject: [PATCH 5/7] chore: fix changeset (#18073)
prevents the changelog from getting borked up
---
.changeset/great-toes-behave.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.changeset/great-toes-behave.md b/.changeset/great-toes-behave.md
index 26e36f70f1..5f2845e364 100644
--- a/.changeset/great-toes-behave.md
+++ b/.changeset/great-toes-behave.md
@@ -2,4 +2,4 @@
'svelte': patch
---
-fix: correct types for `ontoggle` on elements
+fix: correct types for `ontoggle` on `` elements
From 0395ef0df7ff12c4b633b650c5f2db512d382836 Mon Sep 17 00:00:00 2001
From: Simon H <5968653+dummdidumm@users.noreply.github.com>
Date: Mon, 6 Apr 2026 18:11:08 +0200
Subject: [PATCH 6/7] fix: unskip branches of earlier batches after commit
(#18048)
Fixes #17571 where the situation is the following:
A derived creates a new query. That query initializes loading with true.
This means the if block is marked for destruction (therefore effects
inside branch are skipped), but it's not doing that yet because the
query promise is pending. Then query resolves and loading is set back to
false right before resolving, but it's not the same tick so
`loading=false` is a separate thing. Because that later batch doesn't
see any overlap with an earlier batch (the earlier batch did set loading
to true but not via set but indirectly via recreating the query) it
doesn't wait on it and flushes right away. Now the if block is marked as
visible again but the earlier batch doesn't know that if noone unskips
its branch. If we don't do that the render effect that is now dirty as
part of that batch will not run.
---
.changeset/petite-signs-flash.md | 5 +++
.../src/internal/client/reactivity/batch.js | 28 +++++++++++--
.../samples/async-if-block-unskip/_config.js | 30 ++++++++++++++
.../samples/async-if-block-unskip/main.svelte | 40 +++++++++++++++++++
4 files changed, 100 insertions(+), 3 deletions(-)
create mode 100644 .changeset/petite-signs-flash.md
create mode 100644 packages/svelte/tests/runtime-runes/samples/async-if-block-unskip/_config.js
create mode 100644 packages/svelte/tests/runtime-runes/samples/async-if-block-unskip/main.svelte
diff --git a/.changeset/petite-signs-flash.md b/.changeset/petite-signs-flash.md
new file mode 100644
index 0000000000..38a1c7b47c
--- /dev/null
+++ b/.changeset/petite-signs-flash.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+fix: unskip branches of earlier batches after commit
diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js
index f98436e7bc..9c0832150a 100644
--- a/packages/svelte/src/internal/client/reactivity/batch.js
+++ b/packages/svelte/src/internal/client/reactivity/batch.js
@@ -172,6 +172,12 @@ export class Batch {
*/
#skipped_branches = new Map();
+ /**
+ * Inverse of #skipped_branches which we need to tell prior batches to unskip them when committing
+ * @type {Set}
+ */
+ #unskipped_branches = new Set();
+
is_fork = false;
#decrement_queued = false;
@@ -215,28 +221,31 @@ export class Batch {
if (!this.#skipped_branches.has(effect)) {
this.#skipped_branches.set(effect, { d: [], m: [] });
}
+ this.#unskipped_branches.delete(effect);
}
/**
* Remove an effect from the #skipped_branches map and reschedule
* any tracked dirty/maybe_dirty child effects
* @param {Effect} effect
+ * @param {(e: Effect) => void} callback
*/
- unskip_effect(effect) {
+ unskip_effect(effect, callback = (e) => this.schedule(e)) {
var tracked = this.#skipped_branches.get(effect);
if (tracked) {
this.#skipped_branches.delete(effect);
for (var e of tracked.d) {
set_signal_status(e, DIRTY);
- this.schedule(e);
+ callback(e);
}
for (e of tracked.m) {
set_signal_status(e, MAYBE_DIRTY);
- this.schedule(e);
+ callback(e);
}
}
+ this.#unskipped_branches.add(effect);
}
#process() {
@@ -532,6 +541,19 @@ export class Batch {
invariant(batch.#roots.length === 0, 'Batch has scheduled roots');
}
+ // A batch was unskipped in a later batch -> tell prior batches to unskip it, too
+ if (is_earlier) {
+ for (const unskipped of this.#unskipped_branches) {
+ batch.unskip_effect(unskipped, (e) => {
+ if ((e.f & (BLOCK_EFFECT | ASYNC)) !== 0) {
+ batch.schedule(e);
+ } else {
+ batch.#defer_effects([e]);
+ }
+ });
+ }
+ }
+
batch.activate();
/** @type {Set} */
diff --git a/packages/svelte/tests/runtime-runes/samples/async-if-block-unskip/_config.js b/packages/svelte/tests/runtime-runes/samples/async-if-block-unskip/_config.js
new file mode 100644
index 0000000000..d578def483
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/async-if-block-unskip/_config.js
@@ -0,0 +1,30 @@
+import { tick } from 'svelte';
+import { test } from '../../test';
+
+export default test({
+ async test({ assert, target }) {
+ await tick();
+ const [load, resolve] = target.querySelectorAll('button');
+
+ load.click();
+ await tick();
+ assert.htmlEqual(
+ target.innerHTML,
+ `
+
+
+ `
+ );
+
+ resolve.click();
+ await tick();
+ assert.htmlEqual(
+ target.innerHTML,
+ `
+ search search search search
+
+
+ `
+ );
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/async-if-block-unskip/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-if-block-unskip/main.svelte
new file mode 100644
index 0000000000..36ebcd26d1
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/async-if-block-unskip/main.svelte
@@ -0,0 +1,40 @@
+
+
+{query} {await promise}
+
+{#if !promise.loading}
+ {query}
+{/if}
+
+{#if !promise.loading}
+ {await query}
+{/if}
+
+
+
From 8966601dcd14582cd46d4fbb7c5cf1b444292255 Mon Sep 17 00:00:00 2001
From: Rich Harris
Date: Mon, 6 Apr 2026 17:42:32 -0400
Subject: [PATCH 7/7] fix: handle parens in template expressions more robustly
(#18075)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
While looking into #17954 I realised that a) our code for handling
parentheses in expressions is unnecessarily convoluted and b) it doesn't
handle the case where you have an opening parent outside the first
comment — this fails to parse:
```svelte
{(/**/ 42)}
```
This fixes it and simplifies the code a good bit.
### Before submitting the PR, please make sure you do the following
- [x] It's really useful if your PR references an issue where it is
discussed ahead of time. In many cases, features are absent for a
reason. For large changes, please create an RFC:
https://github.com/sveltejs/rfcs
- [x] Prefix your PR title with `feat:`, `fix:`, `chore:`, or `docs:`.
- [x] This message body should clearly illustrate what problems it
solves.
- [x] Ideally, include a test that fails without this PR but passes with
it.
- [x] If this PR changes code within `packages/svelte/src`, add a
changeset (`npx changeset`).
### Tests and linting
- [x] Run the tests with `pnpm test` and lint the project with `pnpm
lint`
---
.changeset/dull-cows-tie.md | 5 ++
.../src/compiler/phases/1-parse/acorn.js | 31 ++++++----
.../compiler/phases/1-parse/read/context.js | 20 +++---
.../phases/1-parse/read/expression.js | 40 +-----------
.../src/compiler/phases/1-parse/state/tag.js | 7 +--
.../parser-modern/samples/parens/input.svelte | 1 +
.../parser-modern/samples/parens/output.json | 61 +++++++++++++++++++
7 files changed, 100 insertions(+), 65 deletions(-)
create mode 100644 .changeset/dull-cows-tie.md
create mode 100644 packages/svelte/tests/parser-modern/samples/parens/input.svelte
create mode 100644 packages/svelte/tests/parser-modern/samples/parens/output.json
diff --git a/.changeset/dull-cows-tie.md b/.changeset/dull-cows-tie.md
new file mode 100644
index 0000000000..9835805dce
--- /dev/null
+++ b/.changeset/dull-cows-tie.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+fix: handle parens in template expressions 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 77ce4a461c..797ab4cea5 100644
--- a/packages/svelte/src/compiler/phases/1-parse/acorn.js
+++ b/packages/svelte/src/compiler/phases/1-parse/acorn.js
@@ -1,5 +1,6 @@
/** @import { Comment, Program } from 'estree' */
/** @import { AST } from '#compiler' */
+/** @import { Parser } from './index.js' */
import * as acorn from 'acorn';
import { walk } from 'zimmerframe';
import { tsPlugin } from '@sveltejs/acorn-typescript';
@@ -66,26 +67,22 @@ export function parse(source, comments, typescript, is_script) {
}
/**
+ * @param {Parser} parser
* @param {string} source
- * @param {Comment[]} comments
- * @param {boolean} typescript
* @param {number} index
* @returns {acorn.Expression & { leadingComments?: CommentWithLocation[]; trailingComments?: CommentWithLocation[]; }}
*/
-export function parse_expression_at(source, comments, typescript, index) {
- const parser = typescript ? ParserWithTS : acorn.Parser;
+export function parse_expression_at(parser, source, index) {
+ const _ = parser.ts ? ParserWithTS : acorn.Parser;
- const { onComment, add_comments } = get_comment_handlers(
- source,
- /** @type {CommentWithLocation[]} */ (comments),
- index
- );
+ const { onComment, add_comments } = get_comment_handlers(source, parser.root.comments, index);
- const ast = parser.parseExpressionAt(source, index, {
+ const ast = _.parseExpressionAt(source, index, {
onComment,
sourceType: 'module',
ecmaVersion: 16,
- locations: true
+ locations: true,
+ preserveParens: true
});
add_comments(ast);
@@ -93,6 +90,18 @@ export function parse_expression_at(source, comments, typescript, index) {
return ast;
}
+/**
+ * @param {acorn.Expression} node
+ * @returns {acorn.Expression}
+ */
+export function remove_parens(node) {
+ return walk(node, null, {
+ ParenthesizedExpression(node, context) {
+ return context.visit(node.expression);
+ }
+ });
+}
+
/**
* Acorn doesn't add comments to the AST by itself. This factory returns the capabilities
* to add them after the fact. They are needed in order to support `svelte-ignore` comments
diff --git a/packages/svelte/src/compiler/phases/1-parse/read/context.js b/packages/svelte/src/compiler/phases/1-parse/read/context.js
index f90d59fa0b..24b7e2c6b0 100644
--- a/packages/svelte/src/compiler/phases/1-parse/read/context.js
+++ b/packages/svelte/src/compiler/phases/1-parse/read/context.js
@@ -1,7 +1,7 @@
/** @import { Pattern } from 'estree' */
/** @import { Parser } from '../index.js' */
import { match_bracket } from '../utils/bracket.js';
-import { parse_expression_at } from '../acorn.js';
+import { parse_expression_at, remove_parens } from '../acorn.js';
import { regex_not_newline_characters } from '../../patterns.js';
import * as e from '../../../errors.js';
@@ -49,14 +49,12 @@ export default function read_pattern(parser) {
space_with_newline =
space_with_newline.slice(0, first_space) + space_with_newline.slice(first_space + 1);
- const expression = /** @type {any} */ (
- parse_expression_at(
- `${space_with_newline}(${pattern_string} = 1)`,
- parser.root.comments,
- parser.ts,
- start - 1
- )
- ).left;
+ /** @type {any} */
+ let expression = remove_parens(
+ parse_expression_at(parser, `${space_with_newline}(${pattern_string} = 1)`, start - 1)
+ );
+
+ expression = expression.left;
expression.typeAnnotation = read_type_annotation(parser);
if (expression.typeAnnotation) {
@@ -92,13 +90,13 @@ function read_type_annotation(parser) {
// parameters as part of a sequence expression instead, and will then error on optional
// parameters (`?:`). Therefore replace that sequence with something that will not error.
parser.template.slice(parser.index).replace(/\?\s*:/g, ':');
- let expression = parse_expression_at(template, parser.root.comments, parser.ts, a);
+ let expression = remove_parens(parse_expression_at(parser, template, a));
// `foo: bar = baz` gets mangled — fix it
if (expression.type === 'AssignmentExpression') {
let b = expression.right.start;
while (template[b] !== '=') b -= 1;
- expression = parse_expression_at(template.slice(0, b), parser.root.comments, parser.ts, a);
+ expression = remove_parens(parse_expression_at(parser, template.slice(0, b), a));
}
// `array as item: string, index` becomes `string, index`, which is mistaken as a sequence expression - fix that
diff --git a/packages/svelte/src/compiler/phases/1-parse/read/expression.js b/packages/svelte/src/compiler/phases/1-parse/read/expression.js
index 5d21f85792..16d4c4e50f 100644
--- a/packages/svelte/src/compiler/phases/1-parse/read/expression.js
+++ b/packages/svelte/src/compiler/phases/1-parse/read/expression.js
@@ -1,6 +1,6 @@
/** @import { Expression } from 'estree' */
/** @import { Parser } from '../index.js' */
-import { parse_expression_at } from '../acorn.js';
+import { parse_expression_at, remove_parens } from '../acorn.js';
import { regex_whitespace } from '../../patterns.js';
import * as e from '../../../errors.js';
import { find_matching_bracket } from '../utils/bracket.js';
@@ -34,50 +34,16 @@ export function get_loose_identifier(parser, opening_token) {
*/
export default function read_expression(parser, opening_token, disallow_loose) {
try {
- let comment_index = parser.root.comments.length;
-
- const node = parse_expression_at(
- parser.template,
- parser.root.comments,
- parser.ts,
- parser.index
- );
-
- let num_parens = 0;
-
- let i = parser.root.comments.length;
- while (i-- > comment_index) {
- const comment = parser.root.comments[i];
- if (comment.end < node.start) {
- parser.index = comment.end;
- break;
- }
- }
-
- for (let i = parser.index; i < /** @type {number} */ (node.start); i += 1) {
- if (parser.template[i] === '(') num_parens += 1;
- }
+ const node = parse_expression_at(parser, parser.template, parser.index);
let index = /** @type {number} */ (node.end);
const last_comment = parser.root.comments.at(-1);
if (last_comment && last_comment.end > index) index = last_comment.end;
- while (num_parens > 0) {
- const char = parser.template[index];
-
- if (char === ')') {
- num_parens -= 1;
- } else if (!regex_whitespace.test(char)) {
- e.expected_token(index, ')');
- }
-
- index += 1;
- }
-
parser.index = index;
- return /** @type {Expression} */ (node);
+ return /** @type {Expression} */ (remove_parens(node));
} catch (err) {
// If we are in an each loop we need the error to be thrown in cases like
// `as { y = z }` so we still throw and handle the error there
diff --git a/packages/svelte/src/compiler/phases/1-parse/state/tag.js b/packages/svelte/src/compiler/phases/1-parse/state/tag.js
index d9518c726f..ff153128a5 100644
--- a/packages/svelte/src/compiler/phases/1-parse/state/tag.js
+++ b/packages/svelte/src/compiler/phases/1-parse/state/tag.js
@@ -392,12 +392,7 @@ function open(parser) {
let function_expression = matched
? /** @type {ArrowFunctionExpression} */ (
- parse_expression_at(
- prelude + `${params} => {}`,
- parser.root.comments,
- parser.ts,
- params_start
- )
+ parse_expression_at(parser, prelude + `${params} => {}`, params_start)
)
: { params: [] };
diff --git a/packages/svelte/tests/parser-modern/samples/parens/input.svelte b/packages/svelte/tests/parser-modern/samples/parens/input.svelte
new file mode 100644
index 0000000000..ad4f4b7940
--- /dev/null
+++ b/packages/svelte/tests/parser-modern/samples/parens/input.svelte
@@ -0,0 +1 @@
+{(/**/ 42)}
diff --git a/packages/svelte/tests/parser-modern/samples/parens/output.json b/packages/svelte/tests/parser-modern/samples/parens/output.json
new file mode 100644
index 0000000000..7a5b4b38d8
--- /dev/null
+++ b/packages/svelte/tests/parser-modern/samples/parens/output.json
@@ -0,0 +1,61 @@
+{
+ "css": null,
+ "js": [],
+ "start": 0,
+ "end": 11,
+ "type": "Root",
+ "fragment": {
+ "type": "Fragment",
+ "nodes": [
+ {
+ "type": "ExpressionTag",
+ "start": 0,
+ "end": 11,
+ "expression": {
+ "type": "Literal",
+ "start": 7,
+ "end": 9,
+ "loc": {
+ "start": {
+ "line": 1,
+ "column": 7
+ },
+ "end": {
+ "line": 1,
+ "column": 9
+ }
+ },
+ "value": 42,
+ "raw": "42",
+ "leadingComments": [
+ {
+ "type": "Block",
+ "value": "",
+ "start": 2,
+ "end": 6
+ }
+ ]
+ }
+ }
+ ]
+ },
+ "options": null,
+ "comments": [
+ {
+ "type": "Block",
+ "value": "",
+ "start": 2,
+ "end": 6,
+ "loc": {
+ "start": {
+ "line": 1,
+ "column": 2
+ },
+ "end": {
+ "line": 1,
+ "column": 6
+ }
+ }
+ }
+ ]
+}