diff --git a/.changeset/common-ways-deny.md b/.changeset/common-ways-deny.md
new file mode 100644
index 0000000000..75e9375965
--- /dev/null
+++ b/.changeset/common-ways-deny.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+fix: don't (re)connect deriveds when read inside branch/root effects
diff --git a/.changeset/curly-wasps-hide.md b/.changeset/curly-wasps-hide.md
new file mode 100644
index 0000000000..7e55d77ba0
--- /dev/null
+++ b/.changeset/curly-wasps-hide.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+fix: skip unnecessary derived effect in earlier batch
diff --git a/.changeset/dull-oranges-fry.md b/.changeset/dull-oranges-fry.md
new file mode 100644
index 0000000000..0efcc6fa2d
--- /dev/null
+++ b/.changeset/dull-oranges-fry.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+fix: avoid declaration tag warning in event handlers
diff --git a/.changeset/shiny-keys-dance.md b/.changeset/shiny-keys-dance.md
new file mode 100644
index 0000000000..20cf754212
--- /dev/null
+++ b/.changeset/shiny-keys-dance.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+fix: transform computed keys in keyed `{#each}` destructuring patterns
diff --git a/.changeset/tame-donkeys-jump.md b/.changeset/tame-donkeys-jump.md
new file mode 100644
index 0000000000..aae38910db
--- /dev/null
+++ b/.changeset/tame-donkeys-jump.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+fix: chain preprocessor sourcemaps with an empty `sources[0]` instead of dropping them
diff --git a/.changeset/young-doodles-beam.md b/.changeset/young-doodles-beam.md
new file mode 100644
index 0000000000..b68f55d241
--- /dev/null
+++ b/.changeset/young-doodles-beam.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+fix: don't treat declaration tags as parts inside each blocks
diff --git a/.gitignore b/.gitignore
index 556cae6344..e4f9f9cf87 100644
--- a/.gitignore
+++ b/.gitignore
@@ -27,3 +27,4 @@ packages/svelte/scripts/_baseline/
benchmarking/.profiles
benchmarking/compare/.results
benchmarking/compare/.profiles
+benchmarking/compare/results.html
diff --git a/benchmarking/benchmarks/reactivity/tests/kairo_broad_block.bench.js b/benchmarking/benchmarks/reactivity/tests/kairo_broad_block.bench.js
new file mode 100644
index 0000000000..e8451ec3fa
--- /dev/null
+++ b/benchmarking/benchmarks/reactivity/tests/kairo_broad_block.bench.js
@@ -0,0 +1,47 @@
+import assert from 'node:assert';
+import * as $ from 'svelte/internal/client';
+import { block } from '../../../../packages/svelte/src/internal/client/reactivity/effects.js';
+
+// Like `kairo_broad`, but each derived is also read by a block effect, as
+// happens with e.g. `{#if derived}` in a component. Measures our #traverse perf better.
+export default () => {
+ let head = $.state(0);
+ let last = head;
+ let counter = 0;
+
+ const destroy = $.effect_root(() => {
+ for (let i = 0; i < 50; i++) {
+ let current = $.derived(() => {
+ return $.get(head) + i;
+ });
+ let current2 = $.derived(() => {
+ return $.get(current) + 1;
+ });
+ block(() => {
+ $.get(current2);
+ });
+ $.render_effect(() => {
+ $.get(current2);
+ counter++;
+ });
+ last = current2;
+ }
+ });
+
+ return {
+ destroy,
+ run() {
+ $.flush(() => {
+ $.set(head, 1);
+ });
+ counter = 0;
+ for (let i = 0; i < 50; i++) {
+ $.flush(() => {
+ $.set(head, i);
+ });
+ assert.equal($.get(last), i + 50);
+ }
+ assert.equal(counter, 50 * 50);
+ }
+ };
+};
diff --git a/benchmarking/benchmarks/reactivity/tests/kairo_deep_block.bench.js b/benchmarking/benchmarks/reactivity/tests/kairo_deep_block.bench.js
new file mode 100644
index 0000000000..a8d9fa8c35
--- /dev/null
+++ b/benchmarking/benchmarks/reactivity/tests/kairo_deep_block.bench.js
@@ -0,0 +1,48 @@
+import assert from 'node:assert';
+import * as $ from 'svelte/internal/client';
+import { block } from '../../../../packages/svelte/src/internal/client/reactivity/effects.js';
+
+let len = 50;
+const iter = 50;
+
+// Like `kairo_deep`, but the derived chain is also read by a block effect, as
+// happens with e.g. `{#if derived}` in a component. Measures our #traverse perf better.
+export default () => {
+ let head = $.state(0);
+ let current = head;
+ for (let i = 0; i < len; i++) {
+ let c = current;
+ current = $.derived(() => {
+ return $.get(c) + 1;
+ });
+ }
+ let counter = 0;
+
+ const destroy = $.effect_root(() => {
+ block(() => {
+ $.get(current);
+ });
+
+ $.render_effect(() => {
+ $.get(current);
+ counter++;
+ });
+ });
+
+ return {
+ destroy,
+ run() {
+ $.flush(() => {
+ $.set(head, 1);
+ });
+ counter = 0;
+ for (let i = 0; i < iter; i++) {
+ $.flush(() => {
+ $.set(head, i);
+ });
+ assert.equal($.get(current), len + i);
+ }
+ assert.equal(counter, iter);
+ }
+ };
+};
diff --git a/benchmarking/compare/generate-report.js b/benchmarking/compare/generate-report.js
index a61f58909b..70f7b30fbb 100644
--- a/benchmarking/compare/generate-report.js
+++ b/benchmarking/compare/generate-report.js
@@ -2,15 +2,27 @@ import fs from 'node:fs';
import path from 'node:path';
import { pathToFileURL } from 'node:url';
-export function generate_report(outdir) {
+const REPORT_DATA_PLACEHOLDER = '%%REPORT_DATA%%';
+const report_template = fs.readFileSync(
+ new URL('./results.template.html', import.meta.url),
+ 'utf-8'
+);
+
+if (!report_template.includes(REPORT_DATA_PLACEHOLDER)) {
+ throw new Error(`Missing ${REPORT_DATA_PLACEHOLDER} in results.template.html`);
+}
+
+export function generate_report(outdir, branches) {
const result_files = fs
.readdirSync(outdir)
- .filter((file) => file.endsWith('.json'))
+ .filter((file) => file.endsWith('.json') && (!branches || branches.includes(file.slice(0, -5))))
.sort((a, b) => a.localeCompare(b));
- const branches = result_files.map((file) => file.slice(0, -5));
+ // always do this so that ordering lines up (branches argument might be passed in a different order than the result files are sorted
+ branches = result_files.map((file) => file.slice(0, -5));
+
const results = result_files.map((file) =>
- JSON.parse(fs.readFileSync(`${outdir}/${file}`, 'utf-8'))
+ JSON.parse(fs.readFileSync(path.join(outdir, file), 'utf-8'))
);
if (results.length === 0) {
@@ -33,11 +45,38 @@ export function generate_report(outdir) {
write('');
- for (let i = 0; i < results[0].length; i += 1) {
- write(`${results[0][i].benchmark}`);
+ // match results by benchmark name — branches may have different benchmark
+ // lists (e.g. a benchmark that only exists on one of the branches), so
+ // pairing by array index would misattribute results
+ const by_name = results.map((result) => new Map(result.map((r) => [r.benchmark, r])));
+
+ /** @type {string[]} */
+ const names = [];
+
+ for (const result of results) {
+ for (const { benchmark } of result) {
+ if (!names.includes(benchmark)) {
+ names.push(benchmark);
+ }
+ }
+ }
+
+ for (const name of names) {
+ const entries = by_name.map((map) => map.get(name));
+ const missing = entries
+ .map((entry, b) => (entry === undefined ? branches[b] : null))
+ .filter((branch) => branch !== null);
+
+ write(`${name}`);
+
+ if (missing.length > 0) {
+ write(` skipped (missing on ${missing.join(', ')})`);
+ write('');
+ continue;
+ }
for (const metric of ['time', 'gc_time']) {
- const times = results.map((result) => +result[i][metric]);
+ const times = entries.map((entry) => +entry[metric]);
let min = Infinity;
let max = -Infinity;
let min_index = -1;
@@ -68,6 +107,32 @@ export function generate_report(outdir) {
write('');
}
+
+ const benchmarks = names.map((name) => ({
+ name,
+ values: by_name.map((map) => {
+ const entry = map.get(name);
+
+ if (entry === undefined) return null;
+
+ return {
+ time: Number(entry.time),
+ gc_time: Number(entry.gc_time)
+ };
+ })
+ }));
+ const data = JSON.stringify({
+ generated_at: new Date().toISOString(),
+ branches,
+ benchmarks
+ })
+ .replaceAll('<', '\\u003c')
+ .replaceAll('\u2028', '\\u2028')
+ .replaceAll('\u2029', '\\u2029');
+ const html_file = path.resolve(outdir, '../results.html');
+
+ fs.writeFileSync(html_file, report_template.replace(REPORT_DATA_PLACEHOLDER, data));
+ console.log(`\nHTML report written to ${html_file}`);
}
function char(i) {
diff --git a/benchmarking/compare/index.js b/benchmarking/compare/index.js
index 9064ee7da9..2e76f46e1b 100644
--- a/benchmarking/compare/index.js
+++ b/benchmarking/compare/index.js
@@ -85,4 +85,4 @@ if (PROFILE_DIR !== null) {
console.log(`\nCPU profiles written to ${PROFILE_DIR}`);
}
-generate_report(outdir);
+generate_report(outdir, requested_branches);
diff --git a/benchmarking/compare/results.template.html b/benchmarking/compare/results.template.html
new file mode 100644
index 0000000000..02f208ac4f
--- /dev/null
+++ b/benchmarking/compare/results.template.html
@@ -0,0 +1,741 @@
+
+
+
+
+
+ Benchmark comparison
+
+
+
+
+
Benchmark comparison
+
+ Runtime results across branches. Green cells are fastest for an entry and red cells expose
+ the largest regressions. Overall runtime normalizes every benchmark to its fastest result
+ before averaging, so long-running entries do not outweigh short ones.
+
+
+
+
+
+
+
+
Branch standings
+
+ Wins count the fastest branch for each comparable entry. Normalized runtime is the
+ average slowdown against each entry's fastest result; lower is better. Click a heading
+ to sort.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Results by benchmark
+
+ Each cell shows runtime, difference from the winner, and GC time. Missing entries are
+ excluded from both standings.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/benchmarking/compare/runner.js b/benchmarking/compare/runner.js
index 31a8e6b44b..fa746f9869 100644
--- a/benchmarking/compare/runner.js
+++ b/benchmarking/compare/runner.js
@@ -1,18 +1,67 @@
+import { fork } from 'node:child_process';
+import { fileURLToPath } from 'node:url';
import { reactivity_benchmarks } from '../benchmarks/reactivity/index.js';
import { with_cpu_profile } from '../utils.js';
-const results = [];
-const PROFILE_DIR = process.env.BENCH_PROFILE_DIR;
+const PROFILE_DIR = process.env.BENCH_PROFILE_DIR ?? null;
+const single = process.env.BENCH_SINGLE;
-for (let i = 0; i < reactivity_benchmarks.length; i += 1) {
- const benchmark = reactivity_benchmarks[i];
+if (single) {
+ // child mode — run a single benchmark and report the result to the parent
+ const benchmark = reactivity_benchmarks.find((b) => b.label === single);
- process.stderr.write(`Running ${i + 1}/${reactivity_benchmarks.length} ${benchmark.label} `);
- results.push({
- benchmark: benchmark.label,
- ...(await with_cpu_profile(PROFILE_DIR, benchmark.label, () => benchmark.fn()))
- });
- process.stderr.write('\x1b[2K\r');
-}
+ if (!benchmark) {
+ throw new Error(`Unknown benchmark ${single}`);
+ }
+
+ const result = await with_cpu_profile(PROFILE_DIR, benchmark.label, () => benchmark.fn());
+
+ // exit via the callback so the message is guaranteed to be delivered
+ /** @type {NodeJS.Process} */ (process).send(result, () => process.exit(0));
+} else {
+ // parent mode — run every benchmark in its own child process, so that
+ // heap/GC/JIT state from one benchmark cannot contaminate the others
+ const filename = fileURLToPath(import.meta.url);
+ const results = [];
+
+ for (let i = 0; i < reactivity_benchmarks.length; i += 1) {
+ const benchmark = reactivity_benchmarks[i];
+
+ process.stderr.write(`Running ${i + 1}/${reactivity_benchmarks.length} ${benchmark.label} `);
+
+ const result = await new Promise((fulfil, reject) => {
+ const child = fork(filename, [], {
+ env: {
+ ...process.env,
+ BENCH_SINGLE: benchmark.label
+ }
+ });
+
+ /** @type {object | null} */
+ let message_received = null;
-process.send(results);
+ child.on('message', (message) => {
+ message_received = /** @type {object} */ (message);
+ });
+
+ child.on('error', reject);
+
+ child.on('exit', (code) => {
+ if (message_received === null) {
+ reject(new Error(`benchmark ${benchmark.label} exited with code ${code}`));
+ } else {
+ fulfil(message_received);
+ }
+ });
+ });
+
+ results.push({
+ benchmark: benchmark.label,
+ .../** @type {object} */ (result)
+ });
+
+ process.stderr.write('\x1b[2K\r');
+ }
+
+ /** @type {NodeJS.Process} */ (process).send(results);
+}
diff --git a/benchmarking/run.js b/benchmarking/run.js
index 80e40a5ff1..e44e816dd0 100644
--- a/benchmarking/run.js
+++ b/benchmarking/run.js
@@ -1,94 +1,149 @@
+import { fork } from 'node:child_process';
+import { fileURLToPath } from 'node:url';
import * as $ from '../packages/svelte/src/internal/client/index.js';
import { reactivity_benchmarks } from './benchmarks/reactivity/index.js';
import { ssr_benchmarks } from './benchmarks/ssr/index.js';
import { with_cpu_profile } from './utils.js';
-// e.g. `pnpm bench kairo` to only run the kairo benchmarks
-const filters = process.argv.slice(2);
-
const PROFILE_DIR = './benchmarking/.profiles';
-const suites = [
- {
- benchmarks: reactivity_benchmarks.filter(
- (b) => filters.length === 0 || filters.some((f) => b.label.includes(f))
- ),
- name: 'reactivity benchmarks'
- },
- {
- benchmarks: ssr_benchmarks.filter(
- (b) => filters.length === 0 || filters.some((f) => b.label.includes(f))
- ),
- name: 'server-side rendering benchmarks'
- }
-].filter((suite) => suite.benchmarks.length > 0);
-
-if (suites.length === 0) {
- console.log('No benchmarks matched provided filters');
- process.exit(1);
-}
-
-const COLUMN_WIDTHS = [25, 9, 9];
-const TOTAL_WIDTH = COLUMN_WIDTHS.reduce((a, b) => a + b);
-
-const pad_right = (str, n) => str + ' '.repeat(n - str.length);
-const pad_left = (str, n) => ' '.repeat(n - str.length) + str;
+const single = process.env.BENCH_SINGLE;
-let total_time = 0;
-let total_gc_time = 0;
+if (single) {
+ // child mode — run a single benchmark and report the result to the parent
+ const benchmark = [...reactivity_benchmarks, ...ssr_benchmarks].find((b) => b.label === single);
-$.push({}, true);
+ if (!benchmark) {
+ throw new Error(`Unknown benchmark ${single}`);
+ }
-try {
- for (const { benchmarks, name } of suites) {
- let suite_time = 0;
- let suite_gc_time = 0;
+ $.push({}, true);
+
+ const result = await with_cpu_profile(PROFILE_DIR, benchmark.label, () => benchmark.fn());
+
+ $.pop();
+
+ // exit via the callback so the message is guaranteed to be delivered
+ /** @type {NodeJS.Process} */ (process).send(result, () => process.exit(0));
+} else {
+ // parent mode — run every benchmark in its own child process, so that
+ // heap/GC/JIT state from one benchmark cannot contaminate the others
+
+ // e.g. `pnpm bench kairo` to only run the kairo benchmarks
+ const filters = process.argv.slice(2);
+
+ const suites = [
+ {
+ benchmarks: reactivity_benchmarks.filter(
+ (b) => filters.length === 0 || filters.some((f) => b.label.includes(f))
+ ),
+ name: 'reactivity benchmarks'
+ },
+ {
+ benchmarks: ssr_benchmarks.filter(
+ (b) => filters.length === 0 || filters.some((f) => b.label.includes(f))
+ ),
+ name: 'server-side rendering benchmarks'
+ }
+ ].filter((suite) => suite.benchmarks.length > 0);
- console.log(`\nRunning ${name}...\n`);
- console.log(
- pad_right('Benchmark', COLUMN_WIDTHS[0]) +
- pad_left('Time', COLUMN_WIDTHS[1]) +
- pad_left('GC time', COLUMN_WIDTHS[2])
- );
- console.log('='.repeat(TOTAL_WIDTH));
+ if (suites.length === 0) {
+ console.log('No benchmarks matched provided filters');
+ process.exit(1);
+ }
- for (const benchmark of benchmarks) {
- const results = await with_cpu_profile(PROFILE_DIR, benchmark.label, () => benchmark.fn());
+ const filename = fileURLToPath(import.meta.url);
+
+ /**
+ * @param {string} label
+ * @returns {Promise<{ time: number, gc_time: number }>}
+ */
+ const run_benchmark = (label) => {
+ return new Promise((fulfil, reject) => {
+ const child = fork(filename, [], {
+ env: {
+ ...process.env,
+ BENCH_SINGLE: label
+ }
+ });
+
+ /** @type {{ time: number, gc_time: number } | null} */
+ let result = null;
+
+ child.on('message', (message) => {
+ result = /** @type {{ time: number, gc_time: number }} */ (message);
+ });
+
+ child.on('error', reject);
+
+ child.on('exit', (code) => {
+ if (result === null) {
+ reject(new Error(`benchmark ${label} exited with code ${code}`));
+ } else {
+ fulfil(result);
+ }
+ });
+ });
+ };
+
+ const COLUMN_WIDTHS = [25, 9, 9];
+ const TOTAL_WIDTH = COLUMN_WIDTHS.reduce((a, b) => a + b);
+
+ /** @type {(str: string, n: number) => string} */
+ const pad_right = (str, n) => str + ' '.repeat(n - str.length);
+ /** @type {(str: string, n: number) => string} */
+ const pad_left = (str, n) => ' '.repeat(n - str.length) + str;
+
+ let total_time = 0;
+ let total_gc_time = 0;
+
+ try {
+ for (const { benchmarks, name } of suites) {
+ let suite_time = 0;
+ let suite_gc_time = 0;
+
+ console.log(`\nRunning ${name}...\n`);
+ console.log(
+ pad_right('Benchmark', COLUMN_WIDTHS[0]) +
+ pad_left('Time', COLUMN_WIDTHS[1]) +
+ pad_left('GC time', COLUMN_WIDTHS[2])
+ );
+ console.log('='.repeat(TOTAL_WIDTH));
+
+ for (const benchmark of benchmarks) {
+ const results = await run_benchmark(benchmark.label);
+ console.log(
+ pad_right(benchmark.label, COLUMN_WIDTHS[0]) +
+ pad_left(results.time.toFixed(2), COLUMN_WIDTHS[1]) +
+ pad_left(results.gc_time.toFixed(2), COLUMN_WIDTHS[2])
+ );
+ total_time += results.time;
+ total_gc_time += results.gc_time;
+ suite_time += results.time;
+ suite_gc_time += results.gc_time;
+ }
+
+ console.log('='.repeat(TOTAL_WIDTH));
console.log(
- pad_right(benchmark.label, COLUMN_WIDTHS[0]) +
- pad_left(results.time.toFixed(2), COLUMN_WIDTHS[1]) +
- pad_left(results.gc_time.toFixed(2), COLUMN_WIDTHS[2])
+ pad_right('suite', COLUMN_WIDTHS[0]) +
+ pad_left(suite_time.toFixed(2), COLUMN_WIDTHS[1]) +
+ pad_left(suite_gc_time.toFixed(2), COLUMN_WIDTHS[2])
);
- total_time += results.time;
- total_gc_time += results.gc_time;
- suite_time += results.time;
- suite_gc_time += results.gc_time;
+ console.log('='.repeat(TOTAL_WIDTH));
}
- console.log('='.repeat(TOTAL_WIDTH));
- console.log(
- pad_right('suite', COLUMN_WIDTHS[0]) +
- pad_left(suite_time.toFixed(2), COLUMN_WIDTHS[1]) +
- pad_left(suite_gc_time.toFixed(2), COLUMN_WIDTHS[2])
- );
- console.log('='.repeat(TOTAL_WIDTH));
- }
-
- if (PROFILE_DIR !== null) {
console.log(`\nCPU profiles written to ${PROFILE_DIR}`);
+ } catch (e) {
+ // eslint-disable-next-line no-console
+ console.error(e);
+ process.exit(1);
}
-} catch (e) {
- // eslint-disable-next-line no-console
- console.error(e);
- process.exit(1);
-}
-$.pop();
+ console.log('');
-console.log('');
-
-console.log(
- pad_right('total', COLUMN_WIDTHS[0]) +
- pad_left(total_time.toFixed(2), COLUMN_WIDTHS[1]) +
- pad_left(total_gc_time.toFixed(2), COLUMN_WIDTHS[2])
-);
+ console.log(
+ pad_right('total', COLUMN_WIDTHS[0]) +
+ pad_left(total_time.toFixed(2), COLUMN_WIDTHS[1]) +
+ pad_left(total_gc_time.toFixed(2), COLUMN_WIDTHS[2])
+ );
+}
diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/function.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/function.js
index 7bdb2243f2..6b3d227eca 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/function.js
+++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/function.js
@@ -18,7 +18,9 @@ export function visit_function(node, context) {
context.next({
...context.state,
- function_depth: context.state.function_depth + 1,
+ // we generally want to use scope.function_depth unless we specifically increased
+ // that in state.function_depth (e.g. a derived)
+ function_depth: Math.max(context.state.scope.function_depth, context.state.function_depth) + 1,
expression: null
});
}
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 a1371b516a..b33eddd461 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
@@ -294,7 +294,10 @@ export function EachBlock(node, context) {
let key_function = b.id('$.index');
if (node.metadata.keyed) {
- const pattern = /** @type {Pattern} */ (node.context); // can only be keyed when a context is provided
+ // can only be keyed when a context is provided
+ const pattern = /** @type {Pattern} */ (
+ context.visit(/** @type {Pattern} */ (node.context), key_state)
+ );
const expression = /** @type {Expression} */ (
context.visit(/** @type {Expression} */ (node.key), key_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 51c84dd01c..c305d53ea5 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
@@ -8,7 +8,7 @@ import { sanitize_template_string } from '../../../../../utils/sanitize_template
import { regex_is_valid_identifier } from '../../../../patterns.js';
import is_reference from 'is-reference';
import { dev, is_ignored, locator, component_name } from '../../../../../state.js';
-import { build_getter } from '../../utils.js';
+import { build_getter, is_state_source } from '../../utils.js';
import { ExpressionMetadata } from '../../../../nodes.js';
/**
@@ -272,6 +272,10 @@ export function build_bind_this(expression, value, { state, visit }) {
const binding = state.scope.get(node.name);
if (!binding) return;
+ // if it is a state or a derived it means is a declaration tag...in that case we don't want to pass the
+ // value but the signal itself or assignment will break
+ if (is_state_source(binding, state.analysis) || binding.kind === 'derived') return;
+
for (const [owner, scope] of state.scopes) {
if (owner.type === 'EachBlock' && scope === binding.scope) {
ids.push(node);
diff --git a/packages/svelte/src/compiler/utils/mapped_code.js b/packages/svelte/src/compiler/utils/mapped_code.js
index 7686ba59c6..635743c0c4 100644
--- a/packages/svelte/src/compiler/utils/mapped_code.js
+++ b/packages/svelte/src/compiler/utils/mapped_code.js
@@ -311,6 +311,13 @@ function apply_preprocessor_sourcemap(filename, svelte_map, preprocessor_map_inp
typeof preprocessor_map_input === 'string'
? JSON.parse(preprocessor_map_input)
: preprocessor_map_input;
+ // A preprocessor map with a missing/empty `sources[0]` (e.g. from a MagicString transform
+ // created without a `source` option) can't be matched against `filename` during combination,
+ // which silently drops the chain instead of erroring. Normalize it to `filename` first, the
+ // same way Vite treats an empty `sources[0]` as referring to the file being transformed.
+ if (preprocessor_map.sources?.length === 1 && !preprocessor_map.sources[0]) {
+ preprocessor_map.sources = [filename];
+ }
const result_map = combine_sourcemaps(filename, [svelte_map, preprocessor_map]);
// Svelte expects a SourceMap which includes toUrl and toString. Instead of wrapping our output in a class,
// we just tack on the extra properties.
diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js
index 59686bca61..37419044bd 100644
--- a/packages/svelte/src/internal/client/runtime.js
+++ b/packages/svelte/src/internal/client/runtime.js
@@ -63,6 +63,9 @@ import { without_reactive_context } from './dom/elements/bindings/shared.js';
import { set_signal_status, update_derived_status } from './reactivity/status.js';
import * as w from './warnings.js';
+/**
+ * True if updating in an effect context that is reactive (i.e. not branch/root effects)
+ */
let is_updating_effect = false;
export let is_destroying_effect = false;
@@ -462,7 +465,7 @@ export function update_effect(effect) {
var was_updating_effect = is_updating_effect;
active_effect = effect;
- is_updating_effect = true;
+ is_updating_effect = (flags & (BRANCH_EFFECT | ROOT_EFFECT)) === 0; // Branch/root effects are not reactive contexts
if (DEV) {
var previous_component_fn = dev_current_component_function;
diff --git a/packages/svelte/tests/runtime-runes/samples/async-batch-derived/_config.js b/packages/svelte/tests/runtime-runes/samples/async-batch-derived/_config.js
new file mode 100644
index 0000000000..d5286d9d95
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/async-batch-derived/_config.js
@@ -0,0 +1,28 @@
+import { tick } from 'svelte';
+import { test } from '../../test';
+
+export default test({
+ async test({ assert, target }) {
+ const [increment, pop] = target.querySelectorAll('button');
+
+ increment.click();
+ await tick();
+ assert.htmlEqual(
+ target.innerHTML,
+ `