Merge branch 'main' into fix/snippet-ts-annotation-error

pull/17954/head
Rich Harris 5 months ago committed by GitHub
commit 35effcb119
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,70 @@
---
name: performance-investigation
description: Investigate performance regressions and find opportunities for optimization
---
## Quick start
1. Start from a branch you want to measure (for example `foo`).
2. Run:
```sh
pnpm bench:compare main foo
```
If you pass one branch, `bench:compare` automatically compares it to `main`.
## Where outputs go
- Summary report: `benchmarking/compare/.results/report.txt`
- Raw benchmark numbers:
- `benchmarking/compare/.results/main.json`
- `benchmarking/compare/.results/<your-branch>.json`
- CPU profiles (per benchmark, per branch):
- `benchmarking/compare/.profiles/main/*.cpuprofile`
- `benchmarking/compare/.profiles/main/*.md`
- `benchmarking/compare/.profiles/<your-branch>/*.cpuprofile`
- `benchmarking/compare/.profiles/<your-branch>/*.md`
The `.md` files are generated summaries of the CPU profile and are usually the fastest way to inspect hotspots.
## Suggested investigation flow
1. Open `benchmarking/compare/.results/report.txt` and identify largest regressions first.
2. For each high-delta benchmark, compare:
- `benchmarking/compare/.profiles/main/<benchmark>.md`
- `benchmarking/compare/.profiles/<branch>/<benchmark>.md`
3. Look for changes in self/inclusive hotspot share in runtime internals (`runtime.js`, `reactivity/batch.js`, `reactivity/deriveds.js`, `reactivity/sources.js`).
4. Make one optimization change at a time, then re-run targeted benches before re-running full compare.
## Fast benchmark loops
Run only selected reactivity benchmarks by substring:
```sh
pnpm bench kairo_mux kairo_deep kairo_broad kairo_triangle
pnpm bench repeated_deps sbench_create_signals mol_owned
```
## Tests to run after perf changes
Runtime reactivity regressions are most likely in runes runtime tests:
```sh
pnpm test runtime-runes
```
## Helpful script
For quick cpuprofile hotspot deltas between two branches:
```sh
node benchmarking/compare/profile-diff.mjs kairo_mux_owned main foo
```
This prints top function sample-share deltas for the selected benchmark.
## Practical gotchas
- `bench:compare` checks out branches while running. Avoid uncommitted changes (or stash them) so branch switching is safe.
- Each `bench:compare` run rewrites `benchmarking/compare/.results` and `benchmarking/compare/.profiles`.

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: invalidate `@const` tags based on visible references in legacy mode

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: disallow `--` in `idPrefix`

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: correct types for `ontoggle` on `<details>` elements

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: don't override `$destroy/set/on` instance methods in dev mode

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: unskip branches of earlier batches after commit

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: never set derived.v inside fork

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: skip rebase logic in non-async mode

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: don't reset status of uninitialized deriveds

@ -4,7 +4,6 @@ root = true
end_of_line = lf
insert_final_newline = true
indent_style = tab
indent_size = 2
charset = utf-8
trim_trailing_whitespace = true

@ -50,7 +50,7 @@ jobs:
if: github.event_name == 'workflow_dispatch' || steps.pr.outcome == 'success'
with:
ref: ${{ github.event_name == 'workflow_dispatch' && github.ref || steps.pr.outputs.ref }}
- uses: pnpm/action-setup@v4.3.0
- uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4.3.0
- uses: actions/setup-node@v6
with:
node-version: 24

@ -33,7 +33,7 @@ jobs:
steps:
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v4
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4
- uses: actions/setup-node@v6
with:
node-version: ${{ matrix.node-version }}
@ -49,7 +49,7 @@ jobs:
timeout-minutes: 10
steps:
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v4
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4
- uses: actions/setup-node@v6
with:
node-version: 22
@ -66,7 +66,7 @@ jobs:
timeout-minutes: 5
steps:
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v4
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4
- uses: actions/setup-node@v6
with:
node-version: 24
@ -83,7 +83,7 @@ jobs:
timeout-minutes: 5
steps:
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v4
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4
- uses: actions/setup-node@v6
with:
node-version: 24
@ -104,10 +104,10 @@ jobs:
timeout-minutes: 15
steps:
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v4
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4
- uses: actions/setup-node@v6
with:
node-version: 18
node-version: 24
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm bench

@ -35,7 +35,7 @@ jobs:
# For push, fall back to the push SHA.
ref: ${{ github.event.pull_request.head.sha || inputs.sha || github.sha }}
- uses: pnpm/action-setup@v4
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4
- uses: actions/setup-node@v6
with:
node-version: 22.x

@ -27,7 +27,7 @@ jobs:
with:
# This makes Actions fetch all Git history so that Changesets can generate changelogs with the correct commits
fetch-depth: 0
- uses: pnpm/action-setup@v4
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4
- name: Setup Node.js
uses: actions/setup-node@v6
with:
@ -42,7 +42,7 @@ jobs:
- name: Create Release Pull Request or Publish to npm
id: changesets
uses: changesets/action@v1
uses: changesets/action@6a0a831ff30acef54f2c6aa1cbbc1096b066edaf # v1
with:
version: pnpm changeset:version
publish: pnpm changeset:publish

2
.gitignore vendored

@ -23,4 +23,6 @@ coverage
tmp
benchmarking/.profiles
benchmarking/compare/.results
benchmarking/compare/.profiles

@ -0,0 +1,9 @@
# Svelte Coding Agent Guide
This guide is for AI coding agents working in the Svelte monorepo.
**Important:** Read and follow [`CONTRIBUTING.md`](./CONTRIBUTING.md) as well - it contains essential information about testing, code structure, and contribution guidelines that applies here.
## Quick Reference
If asked to do a performance investigation, use the `performance-investigation` skill.

@ -1,5 +1,6 @@
import fs from 'node:fs';
import path from 'node:path';
import 'svelte/internal/flags/async';
import {
sbench_create_0to1,
sbench_create_1000to1,

@ -0,0 +1,32 @@
import assert from 'node:assert';
import * as $ from 'svelte/internal/client';
export default () => {
const a = $.state(1);
const b = $.state(2);
let total = 0;
const destroy = $.effect_root(() => {
for (let i = 0; i < 1000; i += 1) {
$.render_effect(() => {
total += $.get(a);
});
}
$.render_effect(() => {
total += $.get(b);
});
});
return {
destroy,
run() {
for (let i = 0; i < 5; i++) {
total = 0;
$.flush(() => $.set(b, i));
assert.equal(total, i);
}
}
};
};

@ -0,0 +1,81 @@
import fs from 'node:fs';
import path from 'node:path';
import { pathToFileURL } from 'node:url';
export function generate_report(outdir) {
const result_files = fs
.readdirSync(outdir)
.filter((file) => file.endsWith('.json'))
.sort((a, b) => a.localeCompare(b));
const branches = result_files.map((file) => file.slice(0, -5));
const results = result_files.map((file) =>
JSON.parse(fs.readFileSync(`${outdir}/${file}`, 'utf-8'))
);
if (results.length === 0) {
console.error(`No result files found in ${outdir}`);
process.exit(1);
}
const report_file = path.join(outdir, 'report.txt');
fs.writeFileSync(report_file, '');
const write = (str) => {
fs.appendFileSync(report_file, str + '\n');
console.log(str);
};
for (let i = 0; i < branches.length; i += 1) {
write(`${char(i)}: ${branches[i]}`);
}
write('');
for (let i = 0; i < results[0].length; i += 1) {
write(`${results[0][i].benchmark}`);
for (const metric of ['time', 'gc_time']) {
const times = results.map((result) => +result[i][metric]);
let min = Infinity;
let max = -Infinity;
let min_index = -1;
for (let b = 0; b < times.length; b += 1) {
const time = times[b];
if (time < min) {
min = time;
min_index = b;
}
if (time > max) {
max = time;
}
}
if (min !== 0) {
write(` ${metric}: fastest is ${char(min_index)} (${branches[min_index]})`);
times.forEach((time, b) => {
const SIZE = 20;
const n = Math.round(SIZE * (time / max));
write(` ${char(b)}: ${'◼'.repeat(n)}${' '.repeat(SIZE - n)} ${time.toFixed(2)}ms`);
});
}
}
write('');
}
}
function char(i) {
return String.fromCharCode(97 + i);
}
if (import.meta.url === pathToFileURL(process.argv[1]).href) {
const outdir = path.resolve(process.argv[1], '../.results');
generate_report(outdir);
}

@ -2,6 +2,8 @@ import fs from 'node:fs';
import path from 'node:path';
import { execSync, fork } from 'node:child_process';
import { fileURLToPath } from 'node:url';
import { safe } from '../utils.js';
import { generate_report } from './generate-report.js';
// if (execSync('git status --porcelain').toString().trim()) {
// console.error('Working directory is not clean');
@ -12,39 +14,61 @@ const filename = fileURLToPath(import.meta.url);
const runner = path.resolve(filename, '../runner.js');
const outdir = path.resolve(filename, '../.results');
if (fs.existsSync(outdir)) fs.rmSync(outdir, { recursive: true });
fs.mkdirSync(outdir);
fs.mkdirSync(outdir, { recursive: true });
const branches = [];
const requested_branches = [];
let PROFILE_DIR = path.resolve(filename, '../.profiles');
fs.mkdirSync(PROFILE_DIR, { recursive: true });
for (const arg of process.argv.slice(2)) {
if (arg.startsWith('--')) continue;
if (arg === filename) continue;
branches.push(arg);
requested_branches.push(arg);
}
if (branches.length === 0) {
branches.push(
if (requested_branches.length === 0) {
requested_branches.push(
execSync('git symbolic-ref --short -q HEAD || git rev-parse --short HEAD').toString().trim()
);
}
if (branches.length === 1) {
branches.push('main');
const original_ref = execSync('git symbolic-ref --short -q HEAD || git rev-parse --short HEAD')
.toString()
.trim();
if (
requested_branches.length === 1 &&
!requested_branches.includes('main') &&
!fs.existsSync(`${outdir}/main.json`)
) {
requested_branches.push('main');
}
process.on('exit', () => {
execSync(`git checkout ${branches[0]}`);
execSync(`git checkout ${original_ref}`);
});
for (const branch of branches) {
for (const branch of requested_branches) {
console.group(`Benchmarking ${branch}`);
const branch_profile_dir = `${PROFILE_DIR}/${safe(branch)}`;
if (fs.existsSync(branch_profile_dir))
fs.rmSync(branch_profile_dir, { recursive: true, force: true });
const branch_result_file = `${outdir}/${branch}.json`;
if (fs.existsSync(branch_result_file)) fs.rmSync(branch_result_file, { force: true });
execSync(`git checkout ${branch}`);
await new Promise((fulfil, reject) => {
const child = fork(runner);
const child = fork(runner, [], {
env: {
...process.env,
BENCH_PROFILE_DIR: branch_profile_dir
}
});
child.on('message', (results) => {
fs.writeFileSync(`${outdir}/${branch}.json`, JSON.stringify(results, null, ' '));
@ -57,47 +81,8 @@ for (const branch of branches) {
console.groupEnd();
}
const results = branches.map((branch) => {
return JSON.parse(fs.readFileSync(`${outdir}/${branch}.json`, 'utf-8'));
});
for (let i = 0; i < results[0].length; i += 1) {
console.group(`${results[0][i].benchmark}`);
for (const metric of ['time', 'gc_time']) {
const times = results.map((result) => +result[i][metric]);
let min = Infinity;
let max = -Infinity;
let min_index = -1;
for (let b = 0; b < times.length; b += 1) {
const time = times[b];
if (time < min) {
min = time;
min_index = b;
}
if (time > max) {
max = time;
}
}
if (min !== 0) {
console.group(`${metric}: fastest is ${char(min_index)} (${branches[min_index]})`);
times.forEach((time, b) => {
const SIZE = 20;
const n = Math.round(SIZE * (time / max));
console.log(`${char(b)}: ${'◼'.repeat(n)}${' '.repeat(SIZE - n)} ${time.toFixed(2)}ms`);
});
console.groupEnd();
}
}
console.groupEnd();
if (PROFILE_DIR !== null) {
console.log(`\nCPU profiles written to ${PROFILE_DIR}`);
}
function char(i) {
return String.fromCharCode(97 + i);
}
generate_report(outdir);

@ -0,0 +1,83 @@
import fs from 'node:fs';
import path from 'node:path';
const [benchmark, baseBranch = 'main', candidateBranch] = process.argv.slice(2);
if (!benchmark || !candidateBranch) {
console.error(
'Usage: node benchmarking/compare/profile-diff.mjs <benchmark> <base-branch> <candidate-branch>'
);
process.exit(1);
}
const root = path.resolve('benchmarking/compare/.profiles');
function safe(name) {
return name.replace(/[^a-z0-9._-]+/gi, '_');
}
function read_profile(branch, bench) {
const file = path.join(root, safe(branch), `${bench}.cpuprofile`);
const profile = JSON.parse(fs.readFileSync(file, 'utf8'));
const nodes = Array.isArray(profile.nodes) ? profile.nodes : [];
const samples = Array.isArray(profile.samples) ? profile.samples : [];
const id_to_node = new Map(nodes.map((node) => [node.id, node]));
const self_counts = new Map();
for (const sample of samples) {
if (typeof sample !== 'number') continue;
self_counts.set(sample, (self_counts.get(sample) ?? 0) + 1);
}
const total = samples.length || 1;
const by_fn = new Map();
for (const [id, count] of self_counts) {
const node = id_to_node.get(id);
if (!node || typeof node !== 'object') continue;
const frame = node.callFrame ?? {};
const function_name = frame.functionName || '(anonymous)';
const url = frame.url || '';
const line = typeof frame.lineNumber === 'number' ? frame.lineNumber + 1 : 0;
const label = url
? `${function_name} @ ${url.replace(/^.*packages\//, 'packages/')}:${line}`
: function_name;
by_fn.set(label, (by_fn.get(label) ?? 0) + count);
}
return { by_fn, total };
}
const base = read_profile(baseBranch, benchmark);
const candidate = read_profile(candidateBranch, benchmark);
const keys = new Set([...base.by_fn.keys(), ...candidate.by_fn.keys()]);
const rows = [...keys]
.map((key) => {
const base_pct = ((base.by_fn.get(key) ?? 0) * 100) / base.total;
const candidate_pct = ((candidate.by_fn.get(key) ?? 0) * 100) / candidate.total;
return {
key,
delta: candidate_pct - base_pct,
base_pct,
candidate_pct
};
})
.sort((a, b) => Math.abs(b.delta) - Math.abs(a.delta))
.slice(0, 20);
console.log(`Benchmark: ${benchmark}`);
console.log(`Base: ${baseBranch}`);
console.log(`Candidate: ${candidateBranch}`);
console.log('');
for (const row of rows) {
const sign = row.delta >= 0 ? '+' : '';
console.log(
`${sign}${row.delta.toFixed(2).padStart(6)}pp candidate ${row.candidate_pct.toFixed(2).padStart(6)}% base ${row.base_pct.toFixed(2).padStart(6)}% ${row.key}`
);
}

@ -1,12 +1,17 @@
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;
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} `);
results.push({ benchmark: benchmark.label, ...(await benchmark.fn()) });
results.push({
benchmark: benchmark.label,
...(await with_cpu_profile(PROFILE_DIR, benchmark.label, () => benchmark.fn()))
});
process.stderr.write('\x1b[2K\r');
}

@ -1,10 +1,13 @@
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(
@ -50,7 +53,7 @@ try {
console.log('='.repeat(TOTAL_WIDTH));
for (const benchmark of benchmarks) {
const results = await benchmark.fn();
const results = await with_cpu_profile(PROFILE_DIR, benchmark.label, () => benchmark.fn());
console.log(
pad_right(benchmark.label, COLUMN_WIDTHS[0]) +
pad_left(results.time.toFixed(2), COLUMN_WIDTHS[1]) +
@ -70,6 +73,10 @@ try {
);
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);

@ -1,4 +1,7 @@
import { performance, PerformanceObserver } from 'node:perf_hooks';
import fs from 'node:fs';
import path from 'node:path';
import inspector from 'node:inspector/promises';
import v8 from 'v8-natives';
// Credit to https://github.com/milomg/js-reactivity-benchmark for the logic for timing + GC tracking.
@ -41,3 +44,290 @@ export async function fastest_test(times, fn) {
return results.reduce((a, b) => (a.time < b.time ? a : b));
}
export function safe(name) {
return name.replace(/[^a-z0-9._-]+/gi, '_');
}
/**
* @param {unknown} value
*/
function format_markdown_value(value) {
if (value === null || value === undefined) return '';
if (Array.isArray(value)) return value.map((item) => String(item)).join(', ');
if (typeof value === 'object') return JSON.stringify(value);
return String(value);
}
/**
* @param {string} text
*/
function escape_markdown_cell(text) {
return text.replace(/\\/g, '\\\\').replace(/\|/g, '\\|').replace(/\r?\n/g, ' ');
}
/**
* @param {string} value
*/
function normalize_profile_url(value) {
if (!value) return '';
if (value.startsWith('file://')) {
try {
const pathname = decodeURIComponent(new URL(value).pathname);
const relative = path.relative(process.cwd(), pathname);
if (relative && !relative.startsWith('..') && !path.isAbsolute(relative)) return relative;
return pathname;
} catch {
return value;
}
}
if (path.isAbsolute(value)) {
const relative = path.relative(process.cwd(), value);
if (relative && !relative.startsWith('..') && !path.isAbsolute(relative)) return relative;
}
return value;
}
/**
* @param {string} function_name
*/
function is_special_runtime_node(function_name) {
return function_name === '(idle)' || function_name === '(garbage collector)';
}
/**
* @param {string} normalized_url
*/
function is_svelte_source_url(normalized_url) {
return normalized_url.startsWith('packages/svelte/');
}
/**
* @param {Record<string, unknown>} profile
*/
function profile_to_markdown(profile) {
/** @type {string[]} */
const lines = ['# CPU profile'];
const metadata = Object.entries(profile).filter(
([key]) => key !== 'nodes' && key !== 'samples' && key !== 'timeDeltas'
);
if (metadata.length > 0) {
lines.push('', '## Metadata', '| Field | Value |', '| --- | --- |');
for (const [key, value] of metadata) {
lines.push(
`| ${escape_markdown_cell(key)} | ${escape_markdown_cell(format_markdown_value(value))} |`
);
}
}
const nodes = Array.isArray(profile.nodes) ? profile.nodes : [];
const samples = Array.isArray(profile.samples) ? profile.samples : [];
const timeDeltas = Array.isArray(profile.timeDeltas) ? profile.timeDeltas : [];
/** @type {Set<number>} */
const included_node_ids = new Set();
if (nodes.length > 0) {
/** @type {Map<number, Record<string, unknown>>} */
const nodes_by_id = new Map();
/** @type {Map<number, number>} */
const parent_by_id = new Map();
for (const node of nodes) {
if (!node || typeof node !== 'object') continue;
if (typeof node.id !== 'number') continue;
nodes_by_id.set(node.id, node);
const children = Array.isArray(node.children) ? node.children : [];
for (const child of children) {
if (typeof child === 'number') {
parent_by_id.set(child, node.id);
}
}
const callFrame =
node.callFrame && typeof node.callFrame === 'object'
? /** @type {Record<string, unknown>} */ (node.callFrame)
: /** @type {Record<string, unknown>} */ ({});
const functionName =
typeof callFrame.functionName === 'string' ? callFrame.functionName : '(anonymous)';
const normalizedUrl =
typeof callFrame.url === 'string' ? normalize_profile_url(callFrame.url) : '';
if (is_special_runtime_node(functionName) || is_svelte_source_url(normalizedUrl)) {
included_node_ids.add(node.id);
}
}
/** @type {Map<number, number>} */
const self_sample_count = new Map();
for (const sample of samples) {
if (typeof sample !== 'number') continue;
if (!included_node_ids.has(sample)) continue;
self_sample_count.set(sample, (self_sample_count.get(sample) ?? 0) + 1);
}
/** @type {Map<number, number>} */
const inclusive_sample_count = new Map();
/** @type {Set<number>} */
const stack = new Set();
/** @param {number} node_id */
const get_inclusive_count = (node_id) => {
const cached = inclusive_sample_count.get(node_id);
if (cached !== undefined) return cached;
if (stack.has(node_id)) return self_sample_count.get(node_id) ?? 0;
stack.add(node_id);
const node = nodes_by_id.get(node_id);
const children = node && Array.isArray(node.children) ? node.children : [];
let total = self_sample_count.get(node_id) ?? 0;
for (const child of children) {
if (typeof child !== 'number') continue;
total += get_inclusive_count(child);
}
stack.delete(node_id);
inclusive_sample_count.set(node_id, total);
return total;
};
for (const node_id of included_node_ids) {
get_inclusive_count(node_id);
}
const total_samples = [...self_sample_count.values()].reduce((sum, count) => sum + count, 0);
if (total_samples > 0) {
const hotspot_rows = [...included_node_ids]
.map((id) => nodes_by_id.get(id))
.filter((node) => !!node)
.map((node) => {
const id = /** @type {number} */ (node.id);
const callFrame =
node.callFrame && typeof node.callFrame === 'object'
? /** @type {Record<string, unknown>} */ (node.callFrame)
: /** @type {Record<string, unknown>} */ ({});
const functionName =
typeof callFrame.functionName === 'string' && callFrame.functionName.length > 0
? callFrame.functionName
: '(anonymous)';
const selfCount = self_sample_count.get(id) ?? 0;
const inclusiveCount = inclusive_sample_count.get(id) ?? selfCount;
return { id, functionName, selfCount, inclusiveCount };
})
.filter((row) => row.selfCount > 0 || row.inclusiveCount > 0)
.sort(
(a, b) =>
b.inclusiveCount - a.inclusiveCount ||
b.selfCount - a.selfCount ||
String(a.id).localeCompare(String(b.id))
)
.slice(0, 25);
if (hotspot_rows.length > 0) {
lines.push(
'',
'## Top hotspots',
'| Rank | Node ID | Function | Self samples | Self % | Inclusive samples | Inclusive % |',
'| --- | --- | --- | --- | --- | --- | --- |'
);
for (let i = 0; i < hotspot_rows.length; i += 1) {
const row = hotspot_rows[i];
const selfPct = ((row.selfCount / total_samples) * 100).toFixed(2);
const inclusivePct = ((row.inclusiveCount / total_samples) * 100).toFixed(2);
lines.push(
`| ${i + 1} | ${row.id} | ${escape_markdown_cell(row.functionName)} | ${row.selfCount} | ${selfPct}% | ${row.inclusiveCount} | ${inclusivePct}% |`
);
}
}
}
lines.push(
'',
'## Nodes',
'| ID | Parent ID | Function | URL | Line | Column | Hit count | Children | Deopt reason |',
'| --- | --- | --- | --- | --- | --- | --- | --- | --- |'
);
for (const node of nodes) {
if (!node || typeof node !== 'object') continue;
if (typeof node.id !== 'number') continue;
if (!included_node_ids.has(node.id)) continue;
const callFrame =
node.callFrame && typeof node.callFrame === 'object'
? /** @type {Record<string, unknown>} */ (node.callFrame)
: /** @type {Record<string, unknown>} */ ({});
const id = typeof node.id === 'number' ? node.id : '';
const parentId =
typeof id === 'number' && included_node_ids.has(parent_by_id.get(id) ?? NaN)
? parent_by_id.get(id) ?? ''
: '';
const functionName =
typeof callFrame.functionName === 'string' && callFrame.functionName.length > 0
? callFrame.functionName
: '(anonymous)';
const url = typeof callFrame.url === 'string' ? normalize_profile_url(callFrame.url) : '';
const lineNumber =
typeof callFrame.lineNumber === 'number' ? String(callFrame.lineNumber + 1) : '';
const columnNumber =
typeof callFrame.columnNumber === 'number' ? String(callFrame.columnNumber + 1) : '';
const hitCount = typeof node.hitCount === 'number' ? node.hitCount : '';
const children = Array.isArray(node.children)
? node.children
.filter((child) => typeof child === 'number' && included_node_ids.has(child))
.join(', ')
: '';
const deoptReason = typeof node.deoptReason === 'string' ? node.deoptReason : '';
lines.push(
`| ${escape_markdown_cell(String(id))} | ${escape_markdown_cell(String(parentId))} | ${escape_markdown_cell(functionName)} | ${escape_markdown_cell(url)} | ${escape_markdown_cell(lineNumber)} | ${escape_markdown_cell(columnNumber)} | ${escape_markdown_cell(String(hitCount))} | ${escape_markdown_cell(children)} | ${escape_markdown_cell(deoptReason)} |`
);
}
}
return `${lines.join('\n')}\n`;
}
/**
* @template T
* @param {string | null} profile_dir
* @param {string} profile_name
* @param {() => T | Promise<T>} fn
* @returns {Promise<T>}
*/
export async function with_cpu_profile(profile_dir, profile_name, fn) {
if (profile_dir === null) {
return await fn();
}
fs.mkdirSync(profile_dir, { recursive: true });
const session = new inspector.Session();
session.connect();
await session.post('Profiler.enable');
await session.post('Profiler.start');
try {
return await fn();
} finally {
const { profile } = /** @type {{ profile: object }} */ (await session.post('Profiler.stop'));
const safe_profile_name = safe(profile_name);
const profile_file = path.join(profile_dir, `${safe_profile_name}.cpuprofile`);
const markdown_file = path.join(profile_dir, `${safe_profile_name}.md`);
fs.writeFileSync(profile_file, JSON.stringify(profile));
fs.writeFileSync(
markdown_file,
profile_to_markdown(/** @type {Record<string, unknown>} */ (profile))
);
session.disconnect();
}
}

@ -51,6 +51,17 @@ In essence, `$derived(expression)` is equivalent to `$derived.by(() => expressio
Anything read synchronously inside the `$derived` expression (or `$derived.by` function body) is considered a _dependency_ of the derived state. When the state changes, the derived will be marked as _dirty_ and recalculated when it is next read.
In addition, if an expression contains an [`await`](await-expressions), 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);
```
...both `a` and `b` are tracked, even though `b` is only read once `a` has resolved, after the initial execution. (This does not apply to `await` in functions that are called by the expression, only the expression itself.)
To exempt a piece of state from being treated as a dependency, use [`untrack`](svelte#untrack).
## Overriding derived values

@ -41,9 +41,11 @@ You can use `$effect` anywhere, not just at the top level of a component, as lon
> [!NOTE] Svelte uses effects internally to represent logic and expressions in your template — this is how `<h1>hello {name}!</h1>` updates when `name` changes.
An effect can return a _teardown function_ which will run immediately before the effect re-runs ([demo](/playground/untitled#H4sIAAAAAAAAE42SQVODMBCF_8pOxkPRKq3HCsx49K4n64xpskjGkDDJ0tph-O8uINo6HjxB3u7HvrehE07WKDbiyZEhi1osRWksRrF57gQdm6E2CKx_dd43zU3co6VB28mIf-nKO0JH_BmRRRVMQ8XWbXkAgfKtI8jhIpIkXKySu7lSG2tNRGZ1_GlYr1ZTD3ddYFmiosUigbyAbpC2lKbwWJkIB8ZhhxBQBWRSw6FCh3sM8GrYTthL-wqqku4N44TyqEgwF3lmRHr4Op0PGXoH31c5rO8mqV-eOZ49bikgtcHBL55tmhIkEMqg_cFB2TpFxjtg703we6NRL8HQFCS07oSUCZi6Rm04lz1yytIHBKoQpo1w6Gsm4gmyS8b8Y5PydeMdX8gwS2Ok4I-ov5NZtvQde95GMsccn_1wzNKfu3RZtS66cSl9lvL7qO1aIk7knbJGvefdtIOzi73M4bYvovUHDFk6AcX_0HRESxnpBOW_jfCDxIZCi_1L_wm4xGQ60wIAAA==)).
An effect can return a _teardown function_ which will run immediately before the effect re-runs:
<!-- codeblock:start {"title":"Effect teardown"} -->
```svelte
<!--- file: App.svelte --->
<script>
let count = $state(0);
let milliseconds = $state(1000);
@ -68,6 +70,7 @@ An effect can return a _teardown function_ which will run immediately before the
<button onclick={() => (milliseconds *= 2)}>slower</button>
<button onclick={() => (milliseconds /= 2)}>faster</button>
```
<!-- codeblock:end -->
Teardown functions also run when the effect is destroyed, which happens when its parent is destroyed (for example, a component is unmounted) or the parent effect re-runs.
@ -206,9 +209,11 @@ Apart from the timing, `$effect.pre` works exactly like `$effect`.
## `$effect.tracking`
The `$effect.tracking` rune is an advanced feature that tells you whether or not the code is running inside a tracking context, such as an effect or inside your template ([demo](/playground/untitled#H4sIAAAAAAAACn3PwYrCMBDG8VeZDYIt2PYeY8Dn2HrIhqkU08nQjItS-u6buAt7UDzmz8ePyaKGMWBS-nNRcmdU-hHUTpGbyuvI3KZvDFLal0v4qvtIgiSZUSb5eWSxPfWSc4oB2xDP1XYk8HHiSHkICeXKeruDDQ4Demlldv4y0rmq6z10HQwuJMxGVv4mVVXDwcJS0jP9u3knynwtoKz1vifT_Z9Jhm0WBCcOTlDD8kyspmML5qNpHg40jc3fFryJ0iWsp_UHgz3180oBAAA=)):
The `$effect.tracking` rune is an advanced feature that tells you whether or not the code is running inside a tracking context, such as an effect or inside your template:
<!-- codeblock:start {"title":"$effect.tracking()"} -->
```svelte
<!--- file: App.svelte --->
<script>
console.log('in component setup:', $effect.tracking()); // false
@ -219,14 +224,27 @@ The `$effect.tracking` rune is an advanced feature that tells you whether or not
<p>in template: {$effect.tracking()}</p> <!-- true -->
```
<!-- codeblock:end -->
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=)):
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:
<!-- codeblock:start {"title":"$effect.pending"} -->
```svelte
<!--- file: App.svelte --->
<script>
let a = $state(1);
let b = $state(2);
async function add(a, b) {
await new Promise((f) => setTimeout(f, 500)); // artificial delay
return a + b;
}
</script>
<button onclick={() => a++}>a++</button>
<button onclick={() => b++}>b++</button>
@ -236,6 +254,7 @@ When using [`await`](await-expressions) in components, the `$effect.pending()` r
<p>pending promises: {$effect.pending()}</p>
{/if}
```
<!-- codeblock:end -->
## `$effect.root`
@ -285,9 +304,11 @@ In general, `$effect` is best considered something of an escape hatch — useful
If you're using an effect because you want to be able to reassign the derived value (to build an optimistic UI, for example) note that [deriveds can be directly overridden]($derived#Overriding-derived-values) as of Svelte 5.25.
You might be tempted to do something convoluted with effects to link one value to another. The following example shows two inputs for "money spent" and "money left" that are connected to each other. If you update one, the other should update accordingly. Don't use effects for this ([demo](/playground/untitled#H4sIAAAAAAAAE5WRTWrDMBCFryKGLBJoY3fRjWIHeoiu6i6UZBwEY0VE49TB-O6VxrFTSih0qe_Ne_OjHpxpEDS8O7ZMeIAnqC1hAP3RA1990hKI_Fb55v06XJA4sZ0J-IjvT47RcYyBIuzP1vO2chVHHFjxiQ2pUr3k-SZRQlbBx_LIFoEN4zJfzQph_UMQr4hRXmBd456Xy5Uqt6pPKHmkfmzyPAZL2PCnbRpg8qWYu63I7lu4gswOSRYqrPNt3CgeqqzgbNwRK1A76w76YqjFspfcQTWmK3vJHlQm1puSTVSeqdOc_r9GaeCHfUSY26TXry6Br4RSK3C6yMEGT-aqVU3YbUZ2NF6rfP2KzXgbuYzY46czdgyazy0On_FlLH3F-UDXhgIO35UGlA1rAgAA)):
You might be tempted to do something convoluted with effects to link one value to another. The following example shows two inputs for "money spent" and "money left" that are connected to each other. If you update one, the other should update accordingly. Instead of using effects for this...
<!-- codeblock:start {"title":"Setting state in effects (don't do this!)"} -->
```svelte
<!--- file: App.svelte --->
<script>
const total = 100;
let spent = $state(0);
@ -311,11 +332,21 @@ You might be tempted to do something convoluted with effects to link one value t
<input type="range" bind:value={left} max={total} />
{left}/{total} left
</label>
<style>
label {
display: flex;
gap: 0.5em;
}
</style>
```
<!-- codeblock:end -->
Instead, use `oninput` callbacks or — better still — [function bindings](bind#Function-bindings) where possible ([demo](/playground/untitled#H4sIAAAAAAAAE5VRvW7CMBB-FcvqECQK6dDFJEgsnfoGTQdDLsjSxVjxhYKivHvPBwFUsXS8774_nwftbQva6I_e78gdvNo6Xzu_j3quG4cQtfkaNJ1DIiWA8atkE8IiHgEpYVsb4Rm-O3gCT2yji7jrXKB15StiOJKiA1lUpXrL81VCEUjFwHTGXiJZgiyf3TYIjSxq6NwR6uyifr0ohMbEZnpHH2rWf7ImS8KZGtK6osl_UqelRIyVL5b3ir5AuwWUtoXzoee6fIWy0p31e6i0XMocLfZQDuI6qtaeykGcR7UU6XWznFAZU9LN_X9B2UyVayk9f3ji0-REugen6U9upDOCcAWcLlS7GNCejWoQTqsLtrfBqHzxDu3DrUTOf0xwIm2o62H85sk6_OHG2jQWI4y_3byXXGMCAAA=)):
...use `oninput` callbacks or — better still — [function bindings](bind#Function-bindings) where possible:
<!-- codeblock:start {"title":"Setting state with function bindings"} -->
```svelte
<!--- file: App.svelte --->
<script>
const total = 100;
let spent = $state(0);
@ -335,6 +366,14 @@ Instead, use `oninput` callbacks or — better still — [function bindings](bin
<input type="range" +++bind:value={() => left, updateLeft}+++ max={total} />
{left}/{total} left
</label>
<style>
label {
display: flex;
gap: 0.5em;
}
</style>
```
<!-- codeblock:end -->
If you absolutely have to update `$state` within an effect and run into an infinite loop because you read and write to the same `$state`, use [untrack](svelte#untrack).

@ -64,8 +64,9 @@ let { a, b, c, ...others } = $props();
## Updating props
References to a prop inside a component update when the prop itself updates — when `count` changes in `App.svelte`, it will also change inside `Child.svelte`. But the child component is able to temporarily override the prop value, which can be useful for unsaved ephemeral state ([demo](/playground/untitled#H4sIAAAAAAAAE6WQ0WrDMAxFf0WIQR0Wmu3VTQJln7HsIfVcZubIxlbGRvC_DzuBraN92qPula50tODZWB1RPi_IX16jLALWSOOUq6P3-_ihLWftNEZ9TVeOWBNHlNhGFYznfqCBzeRdYHh6M_YVzsFNsNs3pdpGd4eBcqPVDMrNxNDBXeSRtXioDgO1zU8ataeZ2RE4Utao924RFXQ9iHXwvoPHKpW1xY4g_Bg0cSVhKS0p560Za95612ZC02ONrD8ZJYdZp_rGQ37ff_mSP86Np2TWZaNNmdcH56P4P67K66_SXoK9pG-5dF5Z9QEAAA==)):
References to a prop inside a component update when the prop itself updates — when `count` changes in `App.svelte`, it will also change inside `Child.svelte`. But the child component is able to temporarily override the prop value, which can be useful for unsaved ephemeral state:
<!-- codeblock:start {"title":"Temporarily updating props","selected":"Child.svelte"} -->
```svelte
<!--- file: App.svelte --->
<script>
@ -91,11 +92,13 @@ References to a prop inside a component update when the prop itself updates —
clicks (child): {count}
</button>
```
<!-- codeblock:end -->
While you can temporarily _reassign_ props, you should not _mutate_ props unless they are [bindable]($bindable).
If the prop is a regular object, the mutation will have no effect ([demo](/playground/untitled#H4sIAAAAAAAAE3WQwU7DMBBEf2W1QmorQgJXk0RC3PkBwiExG9WQrC17U4Es_ztKUkQp9OjxzM7bjcjtSKjwyfKNp1aLORA4b13ADHszUED1HFE-3eyaBcy-Mw_O5eFAg8xa1wb6T9eWhVgCKiyD9sZJ3XAjZnTWCzzuzfAKvbcjbPJieR2jm_uGy-InweXqtd0baaliBG0nFgW3kBIUNWYo9CGoxE-UsgvIpw2_oc9-LmAPJBCPDJCggqvlVtvdH9puErEMlvVg9HsVtzuoaojzkKKAfRuALVDfk5ZZW0fmy05wXcFdwyktlUs-KIinljTXrRVnm7-kL9dYLVbUAQAA)):
If the prop is a regular object, the mutation will have no effect:
<!-- codeblock:start {"title":"Non-reactive props","selected":"Child.svelte"} -->
```svelte
<!--- file: App.svelte --->
<script>
@ -118,9 +121,11 @@ If the prop is a regular object, the mutation will have no effect ([demo](/playg
clicks: {object.count}
</button>
```
<!-- codeblock:end -->
If the prop is a reactive state proxy, however, then mutations _will_ have an effect but you will see an [`ownership_invalid_mutation`](runtime-warnings#Client-warnings-ownership_invalid_mutation) warning, because the component is mutating state that does not 'belong' to it ([demo](/playground/untitled#H4sIAAAAAAAAE3WR0U7DMAxFf8VESBuiauG1WycheOEbKA9p67FA6kSNszJV-XeUZhMw2GN8r-1znUmQ7FGU4pn2UqsOes-SlSGRia3S6ET5Mgk-2OiJBZGdOh6szd0eNcdaIx3-V28NMRI7UYq1awdleVNTzaq3ZmB43CndwXYwPSzyYn4dWxermqJRI4Np3rFlqODasWRcTtAaT1zCHYSbVU3r4nsyrdPMKTUFKDYiE4yfLEoePIbsQpqfy3_nOVMuJIqg0wk1RFg7GOuWfwEbz2wIDLVatR_VtLyBagNTHFIUMCqtoZXeIfAOU1JoUJsR2IC3nWTMjt7GM4yKdyBhlAMpesvhydCC0y_i0ZagHByMh26WzUhXUUxKnpbcVnBfUwhznJnNlac7JkuIURL-2VVfwxflyrWcSQIAAA==)):
If the prop is a reactive state proxy, however, then mutations _will_ have an effect but you will see an [`ownership_invalid_mutation`](runtime-warnings#Client-warnings-ownership_invalid_mutation) warning, because the component is mutating state that does not 'belong' to it:
<!-- codeblock:start {"title":"Invalid mutation","selected":"Child.svelte"} -->
```svelte
<!--- file: App.svelte --->
<script>
@ -147,8 +152,19 @@ If the prop is a reactive state proxy, however, then mutations _will_ have an ef
clicks: {object.count}
</button>
```
<!-- codeblock:end -->
The fallback value of a prop not declared with `$bindable` is left untouched — it is not turned into a reactive state proxy — meaning mutations will not cause updates ([demo](/playground/untitled#H4sIAAAAAAAAE3WQwU7DMBBEf2VkIbUVoYFraCIh7vwA4eC4G9Wta1vxpgJZ_nfkBEQp9OjxzOzTRGHlkUQlXpy9G0gq1idCL43ppDrAD84HUYheGwqieo2CP3y2Z0EU3-En79fhRIaz1slA_-nKWSbLQVRiE9SgPTetbVkfvRsYzztttugHd8RiXU6vr-jisbWb8idhN7O3bEQhmN5ZVDyMlIorcOddv_Eufq4AGmJEuG5PilEjQrnRcoV7JCTUuJlGWq7-YHYjs7NwVhmtDnVcrlA3iLmzLLGTAdaB-j736h68Oxv-JM1I0AFjoG1OzPfX023c1nhobUoT39QeKsRzS8owM8DFTG_pE6dcVl70AQAA))
The fallback value of a prop not declared with `$bindable` is left untouched — it is not turned into a reactive state proxy — meaning mutations will not cause updates:
<!-- codeblock:start {"title":"Non-reactive fallback props","selected":"Child.svelte"} -->
```svelte
<!--- file: App.svelte --->
<script>
import Child from './Child.svelte';
</script>
<Child />
```
```svelte
<!--- file: Child.svelte --->
@ -163,6 +179,7 @@ The fallback value of a prop not declared with `$bindable` is left untouched —
clicks: {object.count}
</button>
```
<!-- codeblock:end -->
In summary: don't mutate props. Either use callback props to communicate changes, or — if parent and child should share the same object — use the [`$bindable`]($bindable) rune.

@ -5,9 +5,11 @@ tags: rune-inspect
> [!NOTE] `$inspect` only works during development. In a production build it becomes a noop.
The `$inspect` rune is roughly equivalent to `console.log`, with the exception that it will re-run whenever its argument changes. `$inspect` tracks reactive state deeply, meaning that updating something inside an object or array using fine-grained reactivity will cause it to re-fire ([demo](/playground/untitled#H4sIAAAAAAAACkWQ0YqDQAxFfyUMhSotdZ-tCvu431AXtGOqQ2NmmMm0LOK_r7Utfby5JzeXTOpiCIPKT5PidkSVq2_n1F7Jn3uIcEMSXHSw0evHpAjaGydVzbUQCmgbWaCETZBWMPlKj29nxBDaHj_edkAiu12JhdkYDg61JGvE_s2nR8gyuBuiJZuDJTyQ7eE-IEOzog1YD80Lb0APLfdYc5F9qnFxjiKWwbImo6_llKRQVs-2u91c_bD2OCJLkT3JZasw7KLA2XCX31qKWE6vIzNk1fKE0XbmYrBTufiI8-_8D2cUWBA_AQAA)):
The `$inspect` rune is roughly equivalent to `console.log`, with the exception that it will re-run whenever its argument changes. `$inspect` tracks reactive state deeply, meaning that updating something inside an object or array using fine-grained reactivity will cause it to re-fire:
<!-- codeblock:start {"title":"$inspect(...)"} -->
```svelte
<!--- file: App.svelte --->
<script>
let count = $state(0);
let message = $state('hello');
@ -18,14 +20,17 @@ The `$inspect` rune is roughly equivalent to `console.log`, with the exception t
<button onclick={() => count++}>Increment</button>
<input bind:value={message} />
```
<!-- codeblock:end -->
On updates, a stack trace will be printed, making it easy to find the origin of a state change (unless you're in the playground, due to technical limitations).
## $inspect(...).with
`$inspect` returns a property `with`, which you can invoke with a callback, which will then be invoked instead of `console.log`. The first argument to the callback is either `"init"` or `"update"`; subsequent arguments are the values passed to `$inspect` ([demo](/playground/untitled#H4sIAAAAAAAACkVQ24qDMBD9lSEUqlTqPlsj7ON-w7pQG8c2VCchmVSK-O-bKMs-DefKYRYx6BG9qL4XQd2EohKf1opC8Nsm4F84MkbsTXAqMbVXTltuWmp5RAZlAjFIOHjuGLOP_BKVqB00eYuKs82Qn2fNjyxLtcWeyUE2sCRry3qATQIpJRyD7WPVMf9TW-7xFu53dBcoSzAOrsqQNyOe2XUKr0Xi5kcMvdDB2wSYO-I9vKazplV1-T-d6ltgNgSG1KjVUy7ZtmdbdjqtzRcphxMS1-XubOITJtPrQWMvKnYB15_1F7KKadA_AQAA)):
`$inspect(...)` returns an object with a `with` method, which you can invoke with a callback that will then be invoked instead of `console.log`. The first argument to the callback is either `"init"` or `"update"`; subsequent arguments are the values passed to `$inspect`:
<!-- codeblock:start {"title":"$inspect(...).with(...)"} -->
```svelte
<!--- file: App.svelte --->
<script>
let count = $state(0);
@ -38,6 +43,7 @@ On updates, a stack trace will be printed, making it easy to find the origin of
<button onclick={() => count++}>Increment</button>
```
<!-- codeblock:end -->
## $inspect.trace(...)

@ -89,9 +89,11 @@ You can freely use destructuring and rest patterns in each blocks.
{#each expression, index}...{/each}
```
In case you just want to render something `n` times, you can omit the `as` part ([demo](/playground/untitled#H4sIAAAAAAAAE3WR0W7CMAxFf8XKNAk0WsSeUEaRpn3Guoc0MbQiJFHiMlDVf18SOrZJ48259_jaVgZmxBEZZ28thgCNFV6xBdt1GgPj7wOji0t2EqI-wa_OleGEmpLWiID_6dIaQkMxhm1UdwKpRQhVzWSaVORJNdvWpqbhAYVsYQCNZk8thzWMC_DCHMZk3wPSThNQ088I3mghD9UwSwHwlLE5PMIzVFUFq3G7WUZ2OyUvU3JOuZU332wCXTRmtPy1NgzXZtUFp8WFw9536uWqpbIgPEaDsJBW90cTOHh0KGi2XsBq5-cT6-3nPauxXqHnsHJnCFZ3CvJVkyuCQ0mFF9TZyCQ162WGvteLKfG197Y3iv_pz_fmS68Hxt8iPBPj5HscP8YvCNX7uhYCAAA=)):
In case you just want to render something `n` times, you can omit the `as` part:
<!-- codeblock:start {"title":"Chess board"} -->
```svelte
<!--- file: App.svelte --->
<div class="chess-board">
{#each { length: 8 }, rank}
{#each { length: 8 }, file}
@ -99,7 +101,22 @@ In case you just want to render something `n` times, you can omit the `as` part
{/each}
{/each}
</div>
<style>
.chess-board {
display: grid;
grid-template-columns: repeat(8, 1fr);
rows: repeat(8, 1fr);
border: 1px solid black;
aspect-ratio: 1;
.black {
background: black;
}
}
</style>
```
<!-- codeblock:end -->
## Else blocks

@ -57,9 +57,11 @@ Like function declarations, snippets can have an arbitrary number of parameters,
## Snippet scope
Snippets can be declared anywhere inside your component. They can reference values declared outside themselves, for example in the `<script>` tag or in `{#each ...}` blocks ([demo](/playground/untitled#H4sIAAAAAAAAE12P0QrCMAxFfyWrwhSEvc8p-h1OcG5RC10bmkyQ0n-3HQPBx3vCPUmCemiDrOpLULYbUdXqTKR2Sj6UA7_RCKbMbvJ9Jg33XpMcW9uKQYEAIzJ3T4QD3LSUDE-PnYA4YET4uOkGMc3W5B3xZrtvbVP9HDas2GqiZHqhMW6Tr9jGbG_oOCMImcUCwrIpFk1FqRyqpRpn0cmjHdAvnrIzuscyq_4nd3dPPD01ukE_NA6qFj9hvMYvGjJADw8BAAA=))...
Snippets can be declared anywhere inside your component. They can reference values declared outside themselves, for example in the `<script>` tag or in `{#each ...}` blocks...
<!-- codeblock:start {"title":"Snippets"} -->
```svelte
<!--- file: App.svelte --->
<script>
let { message = `it's great to see you!` } = $props();
</script>
@ -71,6 +73,7 @@ Snippets can be declared anywhere inside your component. They can reference valu
{@render hello('alice')}
{@render hello('bob')}
```
<!-- codeblock:end -->
...and they are 'visible' to everything in the same lexical scope (i.e. siblings, and children of those siblings):
@ -91,9 +94,11 @@ Snippets can be declared anywhere inside your component. They can reference valu
{@render x()}
```
Snippets can reference themselves and each other ([demo](/playground/untitled#H4sIAAAAAAAAE2WPTQqDMBCFrxLiRqH1Zysi7TlqF1YnENBJSGJLCYGeo5tesUeosfYH3c2bee_jjaWMd6BpfrAU6x5oTvdS0g01V-mFPkNnYNRaDKrxGxto5FKCIaeu1kYwFkauwsoUWtZYPh_3W5FMY4U2mb3egL9kIwY0rbhgiO-sDTgjSEqSTvIDs-jiOP7i_MHuFGAL6p9BtiSbOTl0GtzCuihqE87cqtyam6WRGz_vRcsZh5bmRg3gju4Fptq_kzQBAAA=)):
Snippets can reference themselves and each other:
<!-- codeblock:start {"title":"Self-referencing snippets"} -->
```svelte
<!--- file: App.svelte --->
{#snippet blastoff()}
<span>🚀</span>
{/snippet}
@ -109,14 +114,17 @@ Snippets can reference themselves and each other ([demo](/playground/untitled#H4
{@render countdown(10)}
```
<!-- codeblock:end -->
## Passing snippets to components
### Explicit props
Within the template, snippets are values just like any other. As such, they can be passed to components as props ([demo](/playground/untitled#H4sIAAAAAAAAE3VS247aMBD9lZGpBGwDASRegonaPvQL2qdlH5zYEKvBNvbQLbL875VzAcKyj3PmzJnLGU8UOwqSkd8KJdaCk4TsZS0cyV49wYuJuQiQpGd-N2bu_ooaI1YwJ57hpVYoFDqSEepKKw3mO7VDeTTaIvxiRS1gb_URxvO0ibrS8WanIrHUyiHs7Vmigy28RmyHHmKvDMbMmFq4cQInvGSwTsBYWYoMVhCSB2rBFFPsyl0uruTlR3JZCWvlTXl1Yy_mawiR_rbZKZrellJ-5JQ0RiBUgnFhJ9OGR7HKmwVoilXeIye8DOJGfYCgRlZ3iE876TBsZPX7hPdteO75PC4QaIo8vwNPePmANQ2fMeEFHrLD7rR1jTNkW986E8C3KwfwVr8HSHOSEBT_kGRozyIkn_zQveXDL3rIfPJHtUDwzShJd_Qk3gQCbOGLsdq4yfTRJopRuin3I7nv6kL7ARRjmLdBDG3uv1mhuLA3V2mKtqNEf_oCn8p9aN-WYqH5peP4kWBl1UwJzAEPT9U7K--0fRrrWnPTXpCm1_EVdXjpNmlA8G1hPPyM1fKgMqjFHjctXGjLhZ05w0qpDhksGrybuNEHtJnCalZWsuaTlfq6nPaaBSv_HKw-K57BjzOiVj9ZKQYKzQjZodYFqydYTRN4gPhVzTDO2xnma3HsVWjaLjT8nbfwHy7Q5f2dBAAA)):
Within the template, snippets are values just like any other. As such, they can be passed to components as props:
<!-- codeblock:start {"title":"Explicit snippet props"} -->
```svelte
<!--- file: App.svelte --->
<script>
import Table from './Table.svelte';
@ -141,17 +149,65 @@ Within the template, snippets are values just like any other. As such, they can
<td>{d.qty * d.price}</td>
{/snippet}
<Table data={fruits} {header} {row} />
<Table data={fruits} +++{header} {row}+++ />
```
```svelte
<!--- file: Table.svelte --->
<script>
let { data, header, row } = $props();
</script>
<table>
{#if header}
<thead>
<tr>{@render header()}</tr>
</thead>
{/if}
<tbody>
{#each data as d}
<tr>{@render row(d)}</tr>
{/each}
</tbody>
</table>
<style>
table {
text-align: left;
border-spacing: 0;
}
tbody tr:nth-child(2n+1) {
background: ButtonFace;
}
table :global(th), table :global(td) {
padding: 0.5em;
}
</style>
```
<!-- codeblock:end -->
Think about it like passing content instead of data to a component. The concept is similar to slots in web components.
### Implicit props
As an authoring convenience, snippets declared directly _inside_ a component implicitly become props _on_ the component ([demo](/playground/untitled#H4sIAAAAAAAAE3VSTa_aMBD8Kyu_SkAbCA-JSzBR20N_QXt6vIMTO8SqsY29tI2s_PcqTiB8vaPHs7MzuxuIZgdBMvJLo0QlOElIJZXwJHsLBBvb_XUASc7Mb9Yu_B-hsMMK5sUzvDQahUZPMkJ96aTFfKd3KA_WOISfrFACKmcOMFmk8TWUTjY73RFLoz1C5U4SPWzhrcN2GKDrlcGEWauEnyRwxCaDdQLWyVJksII2uaMWTDPNLtzX5YX8-kgua-GcHJVXI3u5WEPb0d83O03TMZSmfRzOkG1Db7mNacOL19JagVALxoWbztq-H8U6j0SaYp2P2BGbOyQ2v8PQIFMXLKRDk177pq0zf6d8bMrzwBdd0pamyPMb-IjNEzS2f86Gz_Dwf-2F9nvNSUJQ_EOSoTuJNvngqK5v4Pas7n4-OCwlEEJcQTIMO-nSQwtb-GSdsX46e9gbRoP9yGQ11I0rEuycunu6PHx1QnPhxm3SFN15MOlYEFJZtf0dUywMbwZOeBGsrKNLYB54-1R9WNqVdki7usim6VmQphf7mnpshiQRhNAXdoOfMyX3OgMlKtz0cGEcF27uLSul3mewjPjgOOoDukxjPS9rqfh0pb-8zs6aBSt_7505aZ7B9xOi0T9YKW4UooVsr0zB1BTrWQJ3EL-oWcZ572GxFoezCk37QLe3897-B2i2U62uBAAA)):
As an authoring convenience, snippets declared directly _inside_ a component implicitly become props _on_ the component:
<!-- codeblock:start {"title":"Implicit snippet props"} -->
```svelte
<!-- this is semantically the same as the above -->
<!--- file: App.svelte --->
<script>
import Table from './Table.svelte';
const fruits = [
{ name: 'apples', qty: 5, price: 2 },
{ name: 'bananas', qty: 10, price: 1 },
{ name: 'cherries', qty: 20, price: 0.5 }
];
</script>
<Table data={fruits}>
{#snippet header()}
<th>fruit</th>
@ -169,12 +225,54 @@ As an authoring convenience, snippets declared directly _inside_ a component imp
</Table>
```
```svelte
<!--- file: Table.svelte --->
<script>
let { data, header, row } = $props();
</script>
<table>
{#if header}
<thead>
<tr>{@render header()}</tr>
</thead>
{/if}
<tbody>
{#each data as d}
<tr>{@render row(d)}</tr>
{/each}
</tbody>
</table>
<style>
table {
text-align: left;
border-spacing: 0;
}
tbody tr:nth-child(2n+1) {
background: ButtonFace;
}
table :global(th), table :global(td) {
padding: 0.5em;
}
</style>
```
<!-- codeblock:end -->
### Implicit `children` snippet
Any content inside the component tags that is _not_ a snippet declaration implicitly becomes part of the `children` snippet ([demo](/playground/untitled#H4sIAAAAAAAAE3WOQQrCMBBFrzIMggql3ddY1Du4si5sOmIwnYRkFKX07lKqglqX8_7_w2uRDw1hjlsWI5ZqTPBoLEXMdy3K3fdZDzB5Ndfep_FKVnpWHSKNce1YiCVijirqYLwUJQOYxrsgsLmIOIZjcA1M02w4n-PpomSVvTclqyEutDX6DA2pZ7_ABIVugrmEC3XJH92P55_G39GodCmWBFrQJ2PrQAwdLGHig_NxNv9xrQa1dhWIawrv1Wzeqawa8953D-8QOmaEAQAA)):
Any content inside the component tags that is _not_ a snippet declaration implicitly becomes part of the `children` snippet:
<!-- codeblock:start {"title":"Implicit children snippet","selected":"Button.svelte"} -->
```svelte
<!--- file: App.svelte --->
<script>
import Button from './Button.svelte';
</script>
<Button>click me</Button>
```
@ -187,6 +285,7 @@ Any content inside the component tags that is _not_ a snippet declaration implic
<!-- result will be <button>click me</button> -->
<button>{@render children()}</button>
```
<!-- codeblock:end -->
> [!NOTE] Note that you cannot have a prop called `children` if you also have content inside the component — for this reason, you should avoid having props with that name
@ -256,9 +355,21 @@ We can tighten things up further by declaring a generic, so that `data` and `row
## Exporting snippets
Snippets declared at the top level of a `.svelte` file can be exported from a `<script module>` for use in other components, provided they don't reference any declarations in a non-module `<script>` (whether directly or indirectly, via other snippets) ([demo](/playground/untitled#H4sIAAAAAAAAE3WPwY7CMAxEf8UyB1hRgdhjl13Bga8gHFJipEqtGyUGFUX5dxJUtEB3b9bYM_MckHVLWOKut50TMuC5tpbEY4GnuiGP5T6gXG0-ykLSB8vW2oW_UCNZq7Snv_Rjx0Kc4kpc-6OrrfwoVlK3uQ4CaGMgwsl1LUwXy0f54J9-KV4vf20cNo7YkMu22aqAz4-oOLUI9YKluDPF4h_at-hX5PFyzA1tZ84N3fGpf8YfUU6GvDumLqDKmEqCjjCHUEX4hqDTWCU5PJ6Or38c4g1cPu9tnAEAAA==)):
Snippets declared at the top level of a `.svelte` file can be exported from a `<script module>` for use in other components, provided they don't reference any declarations in a non-module `<script>` (whether directly or indirectly, via other snippets):
<!-- codeblock:start {"title":"Exported snippets","selected":"snippets.svelte"} -->
```svelte
<!--- file: App.svelte --->
<script>
import { add } from './snippets.svelte';
</script>
{@render add(1, 2)}
```
```svelte
<!--- file: snippets.svelte --->
<script module>
export { add };
</script>
@ -267,6 +378,7 @@ Snippets declared at the top level of a `.svelte` file can be exported from a `<
{a} + {b} = {a + b}
{/snippet}
```
<!-- codeblock:end -->
> [!NOTE]
> This requires Svelte 5.5.0 or newer

@ -54,9 +54,11 @@ A `bind:value` directive on an `<input>` element binds the input's `value` prope
<p>{message}</p>
```
In the case of a numeric input (`type="number"` or `type="range"`), the value will be coerced to a number ([demo](/playground/untitled#H4sIAAAAAAAAE6WPwYoCMQxAfyWEPeyiOOqx2w74Hds9pBql0IllmhGXYf5dKqwiyILsLXnwwsuI-5i4oPkaUX8yo7kCnKNQV7dNzoty4qSVBSr8jG-Poixa0KAt2z5mbb14TaxA4OCtKCm_rz4-f2m403WltrlrYhMFTtcLNkoeFGqZ8yhDF7j3CCHKzpwoDexGmqCL4jwuPUJHZ-dxVcfmyYGe5MAv-La5pbxYFf5Z9Zf_UJXb-sEMquFgJJhBmGyTW5yj8lnRaD_w9D1dAKSSj7zqAQAA)):
In the case of a numeric input (`type="number"` or `type="range"`), the value will be coerced to a number:
<!-- codeblock:start {"title":"Numeric bindings"} -->
```svelte
<!--- file: App.svelte --->
<script>
let a = $state(1);
let b = $state(2);
@ -74,6 +76,7 @@ In the case of a numeric input (`type="number"` or `type="range"`), the value wi
<p>{a} + {b} = {a + b}</p>
```
<!-- codeblock:end -->
If the input is empty or invalid (in the case of `type="number"`), the value is `undefined`.
@ -144,10 +147,11 @@ Checkboxes can be in an [indeterminate](https://developer.mozilla.org/en-US/docs
## `<input bind:group>`
Inputs that work together can use `bind:group` ([demo](/playground/untitled#H4sIAAAAAAAAE62T32_TMBDH_5XDQkpbrct7SCMGEvCEECDxsO7BSW6L2c227EvbKOv_jp0f6jYhQKJv5_P3PvdL1wstH1Bk4hMSGdgbRzUssFaM9VJciFtF6EV23QvubNRFR_BPUVfWXvodEkdfKT3-zl8Zzag5YETuK6csF1u9ZUIGNo4VkYQNvPYsGRfJF5JKJ8s3QRJE6WoFb2Nq6K-ck13u2Sl9Vxxhlc6QUBIFnz9Brm9ifJ6esun81XoNd860FmtwslYGlLYte5AO4aHlVhJ1gIeKWq92COt1iMtJlkhFPkgh1rHZiiF6K6BUus4G5KafGznCTlIbVUMfQZUWMJh5OrL-C_qjMYSwb1DyiH7iOEuCb1ZpWTUjfHqcwC_GWDVY3ZfmME_SGttSmD9IHaYatvWHIc6xLyqad3mq6KuqcCwnWn9p8p-p71BqP2IH81zc9w2in-od7XORP7ayCpd5YCeXI_-p59mObPF9WmwGpx3nqS2Gzw8TO3zOaS5_GqUXyQUkS3h8hOSz0ZhMESHGc0c4Hm3MAn00t1wrb0l2GZRkqvt4sXwczm6Qh8vnUJzI2LV4vAkvqWgfehTZrSSPx19WiVfFfAQAAA==)):
Inputs that work together can use `bind:group`:
<!-- codeblock:start {"title":"bind:group"} -->
```svelte
<!--- file: BurritoChooser.svelte --->
<!--- file: App.svelte --->
<script>
let tortilla = $state('Plain');
@ -155,6 +159,8 @@ Inputs that work together can use `bind:group` ([demo](/playground/untitled#H4sI
let fillings = $state([]);
</script>
<h1>Customize your burrito</h1>
<!-- grouped radio inputs are mutually exclusive -->
<label><input type="radio" bind:group={tortilla} value="Plain" /> Plain</label>
<label><input type="radio" bind:group={tortilla} value="Whole wheat" /> Whole wheat</label>
@ -165,7 +171,17 @@ Inputs that work together can use `bind:group` ([demo](/playground/untitled#H4sI
<label><input type="checkbox" bind:group={fillings} value="Beans" /> Beans</label>
<label><input type="checkbox" bind:group={fillings} value="Cheese" /> Cheese</label>
<label><input type="checkbox" bind:group={fillings} value="Guac (extra)" /> Guac (extra)</label>
<p>Tortilla: {tortilla}</p>
<p>Fillings: {fillings.join(', ') || 'None'}</p>
<style>
label {
display: block;
}
</style>
```
<!-- codeblock:end -->
> [!NOTE] `bind:group` only works if the inputs are in the same Svelte component.

@ -42,3 +42,9 @@ even over `!important` properties:
<div style:color="red" style="color: blue">This will be red</div>
<div style:color="red" style="color: blue !important">This will still be red</div>
```
You can set CSS custom properties:
```svelte
<div style:--columns={columns}>...</div>
```

@ -25,9 +25,11 @@ The experimental flag will be removed in Svelte 6.
## Synchronized updates
When an `await` expression depends on a particular piece of state, changes to that state will not be reflected in the UI until the asynchronous work has completed, so that the UI is not left in an inconsistent state. In other words, in an example like [this](/playground/untitled#H4sIAAAAAAAAE42QsWrDQBBEf2VZUkhYRE4gjSwJ0qVMkS6XYk9awcFpJe5Wdoy4fw-ycdykSPt2dpiZFYVGxgrf2PsJTlPwPWTcO-U-xwIH5zli9bminudNtwEsbl-v8_wYj-x1Y5Yi_8W7SZRFI1ZYxy64WVsjRj0rEDTwEJWUs6f8cKP2Tp8vVIxSPEsHwyKdukmA-j6jAmwO63Y1SidyCsIneA_T6CJn2ZBD00Jk_XAjT4tmQwEv-32eH6AsgYK6wXWOPPTs6Xy1CaxLECDYgb3kSUbq8p5aaifzorCt0RiUZbQcDIJ10ldH8gs3K6X2Xzqbro5zu1KCHaw2QQPrtclvwVSXc2sEC1T-Vqw0LJy-ClRy_uSkx2ogHzn9ADZ1CubKAQAA)...
When an `await` expression depends on a particular piece of state, changes to that state will not be reflected in the UI until the asynchronous work has completed, so that the UI is not left in an inconsistent state. In other words, in an example like this...
<!-- codeblock:start {"title":"Synchronized updates"} -->
```svelte
<!--- file: App.svelte --->
<script>
let a = $state(1);
let b = $state(2);
@ -43,6 +45,7 @@ When an `await` expression depends on a particular piece of state, changes to th
<p>{a} + {b} = {await add(a, b)}</p>
```
<!-- codeblock:end -->
...if you increment `a`, the contents of the `<p>` will _not_ immediately update to read this —
@ -59,8 +62,8 @@ Updates can overlap — a fast update will be reflected in the UI while an earli
Svelte will do as much asynchronous work as it can in parallel. For example if you have two `await` expressions in your markup...
```svelte
<p>{await one()}</p>
<p>{await two()}</p>
<p>{await one(x)}</p>
<p>{await two(y)}</p>
```
...both functions will run at the same time, as they are independent expressions, even though they are _visually_ sequential.
@ -68,13 +71,18 @@ Svelte will do as much asynchronous work as it can in parallel. For example if y
This does not apply to sequential `await` expressions inside your `<script>` or inside async functions — these run like any other asynchronous JavaScript. An exception is that independent `$derived` expressions will update independently, even though they will run sequentially when they are first created:
```js
async function one() { return 1; }
async function two() { return 2; }
/** @param {number} x */
async function one(x) { return x; }
/** @param {number} y */
async function two(y) { return y; }
let x = $state(1);
let y = $state(2);
// ---cut---
// these will run sequentially the first time,
// but will update independently
let a = $derived(await one());
let b = $derived(await two());
// `b` will not be created until `a` has resolved,
// but once created they will update independently
// even if `x` and `y` update simultaneously
let a = $derived(await one(x));
let b = $derived(await two(y));
```
> [!NOTE] If you write code like this, expect Svelte to give you an [`await_waterfall`](runtime-warnings#Client-warnings-await_waterfall) warning

@ -2,7 +2,66 @@
title: Context
---
Context allows components to access values owned by parent components without passing them down as props (potentially through many layers of intermediate components, known as 'prop-drilling'). The parent component sets context with `setContext(key, value)`...
Context allows components to access values owned by parent components without passing them down as props (potentially through many layers of intermediate components, known as 'prop-drilling').
By creating a `[get, set]` pair of functions with `createContext`, you can set the context in a parent component and get it in a child component:
<!-- codeblock:start {"title":"Context","selected":"context.ts"} -->
```svelte
<!--- file: App.svelte --->
<script>
import Parent from './Parent.svelte';
import Child from './Child.svelte';
</script>
<Parent>
<Child />
</Parent>
```
```svelte
<!--- file: Parent.svelte --->
<script>
import { setUserContext } from './context';
let { children } = $props();
setUserContext({ name: 'world' });
</script>
{@render children()}
```
```svelte
<!--- file: Child.svelte --->
<script>
import { getUserContext } from './context';
const user = getUserContext();
</script>
<h1>hello {user.name}, inside Child.svelte</h1>
```
```ts
/// file: context.ts
import { createContext } from 'svelte';
interface User {
name: string;
}
export const [getUserContext, setUserContext] = createContext<User>();
```
<!-- codeblock:end -->
> [!NOTE] `createContext` was added in version 5.40. If you are using an earlier version of Svelte, you must use `setContext` and `getContext` instead.
This is particularly useful when `Parent.svelte` is not directly aware of `Child.svelte`, but instead renders it as part of a `children` [snippet](snippet) as shown above.
## `setContext` and `getContext`
As an alternative to `createContext`, you can use `setContext` and `getContext` directly. The parent component sets context with `setContext(key, value)`...
```svelte
<!--- file: Parent.svelte --->
@ -26,32 +85,28 @@ Context allows components to access values owned by parent components without pa
<h1>{message}, inside Child.svelte</h1>
```
This is particularly useful when `Parent.svelte` is not directly aware of `Child.svelte`, but instead renders it as part of a `children` [snippet](snippet) ([demo](/playground/untitled#H4sIAAAAAAAAE42Q3W6DMAyFX8WyJgESK-oto6hTX2D3YxcM3IIUQpR40yqUd58CrCXsp7tL7HNsf2dAWXaEKR56yfTBGOOxFWQwfR6Qz8q1XAHjL-GjUhvzToJd7bU09FO9ctMkG0wxM5VuFeeFLLjtVK8ZnkpNkuGo-w6CTTJ9Z3PwsBAemlbUF934W8iy5DpaZtOUcU02-ZLcaS51jHEkTFm_kY1_wfOO8QnXrb8hBzDEc6pgZ4gFoyz4KgiD7nxfTe8ghqAhIfrJ46cTzVZBbkPlODVJsLCDO6V7ZcJoncyw1yRr0hd1GNn_ZbEM3I9i1bmVxOlWElUvDUNHxpQngt3C4CXzjS1rtvkw22wMrTRtTbC8Lkuabe7jvthPPe3DofYCAAA=)):
```svelte
<Parent>
<Child />
</Parent>
```
The key (`'my-context'`, in the example above) and the context itself can be any JavaScript value.
> [!NOTE] `createContext` is preferred since it provides better type safety and makes it unnecessary to use keys.
In addition to [`setContext`](svelte#setContext) and [`getContext`](svelte#getContext), Svelte exposes [`hasContext`](svelte#hasContext) and [`getAllContexts`](svelte#getAllContexts) functions.
## Using context with state
You can store reactive state in context ([demo](/playground/untitled#H4sIAAAAAAAAE41R0W6DMAz8FSuaBNUQdK8MkKZ-wh7HHihzu6hgosRMm1D-fUpSVNq12x4iEvvOx_kmQU2PIhfP3DCCJGgHYvxkkYid7NCI_GUS_KUcxhVEMjOelErNB3bsatvG4LW6n0ZsRC4K02qpuKqpZtmrQTNMYJA3QRAs7PTQQxS40eMCt3mX3duxnWb-lS5h7nTI0A4jMWoo4c44P_Hku-zrOazdy64chWo-ScfRkRgl8wgHKrLTH1OxHZkHgoHaTraHcopXUFYzPPVfuC_hwQaD1GrskdiNCdQwJljJqlvXfyqVsA5CGg0uRUQifHw56xFtciO75QrP07vo_JXf_tf8yK2ezDKY_ZWt_1y2qqYzv7bI1IW1V_sN19m-07wCAAA=))...
You can store reactive state in context...
<!-- codeblock:start {"title":"Context with state"} -->
```svelte
<!--- file: App.svelte --->
<script>
import { setContext } from 'svelte';
import { setCounter } from './context.ts';
import Child from './Child.svelte';
let counter = $state({
count: 0
});
setContext('counter', counter);
setCounter(counter);
</script>
<button onclick={() => counter.count += 1}>
@ -61,12 +116,39 @@ You can store reactive state in context ([demo](/playground/untitled#H4sIAAAAAAA
<Child />
<Child />
<Child />
<button onclick={() => counter.count = 0}>
reset
</button>
```
```svelte
<!--- file: Child.svelte --->
<script>
import { getCounter } from './context.ts';
const counter = getCounter();
</script>
<p>{counter.count}</p>
```
```ts
/// file: context.ts
import { createContext } from 'svelte';
interface Counter {
count: number;
}
export const [getCounter, setCounter] = createContext<Counter>();
```
<!-- codeblock:end -->
...though note that if you _reassign_ `counter` instead of updating it, you will 'break the link' — in other words instead of this...
```svelte
<button onclick={() => counter = { count: 0 }}>
<button onclick={() => counter = { count: 0 } }>
reset
</button>
```
@ -81,21 +163,7 @@ You can store reactive state in context ([demo](/playground/untitled#H4sIAAAAAAA
Svelte will warn you if you get it wrong.
## Type-safe context
As an alternative to using `setContext` and `getContext` directly, you can use them via `createContext`. This gives you type safety and makes it unnecessary to use a key:
```ts
/// file: context.ts
// @filename: ambient.d.ts
interface User {}
// @filename: index.ts
// ---cut---
import { createContext } from 'svelte';
export const [getUserContext, setUserContext] = createContext<User>();
```
## Component testing
When writing [component tests](testing#Unit-and-component-tests-with-Vitest-Component-testing), it can be useful to create a wrapper component that sets the context in order to check the behaviour of a component that uses it. As of version 5.49, you can do this sort of thing:
@ -140,7 +208,7 @@ export const myGlobalState = $state({
In many cases this is perfectly fine, but there is a risk: if you mutate the state during server-side rendering (which is discouraged, but entirely possible!)...
```svelte
<!--- file: App.svelte ---->
<!--- file: App.svelte --->
<script>
import { myGlobalState } from './state.svelte.js';

@ -40,22 +40,9 @@ If you want to use one of these features, you need to setup up a `script` prepro
To use non-type-only TypeScript features within Svelte components, you need to add a preprocessor that will turn TypeScript into JavaScript.
```ts
/// file: svelte.config.js
// @noErrors
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
### Using Vite
const config = {
// Note the additional `{ script: true }`
preprocess: vitePreprocess({ script: true })
};
export default config;
```
### Using SvelteKit or Vite
The easiest way to get started is scaffolding a new SvelteKit project by typing `npx sv create`, following the prompts and choosing the TypeScript option.
If you're using SvelteKit, or Vite _without_ SvelteKit, you can use `vitePreprocess` from `@sveltejs/vite-plugin-svelte` in your config file:
```ts
/// file: svelte.config.js
@ -63,19 +50,16 @@ The easiest way to get started is scaffolding a new SvelteKit project by typing
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
const config = {
preprocess: vitePreprocess()
// Note the additional `{ script: true }`
preprocess: vitePreprocess({ script: true })
};
export default config;
```
If you don't need or want all the features SvelteKit has to offer, you can scaffold a Svelte-flavoured Vite project instead by typing `npm create vite@latest` and selecting the `svelte-ts` option.
In both cases, a `svelte.config.js` with `vitePreprocess` will be added. Vite/SvelteKit will read from this config file.
### Other build tools
### Using other build tools
If you're using tools like Rollup or Webpack instead, install their respective Svelte plugins. For Rollup that's [rollup-plugin-svelte](https://github.com/sveltejs/rollup-plugin-svelte) and for Webpack that's [svelte-loader](https://github.com/sveltejs/svelte-loader). For both, you need to install `typescript` and `svelte-preprocess` and add the preprocessor to the plugin config (see the respective READMEs for more info).
If you're using tools like Rollup (via [rollup-plugin-svelte](https://github.com/sveltejs/rollup-plugin-svelte)) or Webpack (via [svelte-loader](https://github.com/sveltejs/svelte-loader)) instead, install `typescript` and `svelte-preprocess` and add the preprocessor to the plugin config. See the respective plugin READMEs for more info.
> [!NOTE] If you're starting a new project, we recommend using SvelteKit or Vite instead

@ -396,7 +396,7 @@ Invalid selector
### declaration_duplicate_module_import
```
Cannot declare a variable with the same name as an import inside `<script module>`
Cannot declare a variable with the same name as an import from `<script module>`
```
### derived_invalid_export

@ -69,6 +69,12 @@ Cause:
`csp.nonce` was set while `csp.hash` was `true`. These options cannot be used simultaneously.
```
### invalid_id_prefix
```
The `idPrefix` option cannot include `--`.
```
### lifecycle_function_unavailable
```

@ -1,5 +1,45 @@
# svelte
## 5.55.1
### Patch Changes
- fix: correctly handle bindings on the server ([#18009](https://github.com/sveltejs/svelte/pull/18009))
- fix: prevent hydration error on async `{@html ...}` ([#17999](https://github.com/sveltejs/svelte/pull/17999))
- fix: cleanup `superTypeParameters` in `ClassDeclarations`/`ClassExpression` ([#18015](https://github.com/sveltejs/svelte/pull/18015))
- fix: improve duplicate module import error message ([#18016](https://github.com/sveltejs/svelte/pull/18016))
- fix: reschedule new effects in prior batches ([#18021](https://github.com/sveltejs/svelte/pull/18021))
## 5.55.0
### Minor Changes
- feat: export TweenOptions, SpringOptions, SpringUpdateOptions and Updater from svelte/motion ([#17967](https://github.com/sveltejs/svelte/pull/17967))
### Patch Changes
- fix: ensure HMR wrapper forwards correct start/end nodes to active effect ([#17985](https://github.com/sveltejs/svelte/pull/17985))
## 5.54.1
### Patch Changes
- fix: hydration comments during hmr ([#17975](https://github.com/sveltejs/svelte/pull/17975))
- fix: null out `effect.b` in `destroy_effect` ([#17980](https://github.com/sveltejs/svelte/pull/17980))
- fix: group sync statements ([#17977](https://github.com/sveltejs/svelte/pull/17977))
- fix: defer batch resolution until earlier intersecting batches have committed ([#17162](https://github.com/sveltejs/svelte/pull/17162))
- fix: properly invoke `iterator.return()` during reactivity loss check ([#17966](https://github.com/sveltejs/svelte/pull/17966))
- fix: remove trailing semicolon from {@const} tag printer ([#17962](https://github.com/sveltejs/svelte/pull/17962))
## 5.54.0
### Minor Changes

@ -952,9 +952,9 @@ export interface HTMLDetailsAttributes extends HTMLAttributes<HTMLDetailsElement
'bind:open'?: boolean | undefined | null;
'on:toggle'?: EventHandler<Event, HTMLDetailsElement> | undefined | null;
ontoggle?: EventHandler<Event, HTMLDetailsElement> | undefined | null;
ontogglecapture?: EventHandler<Event, HTMLDetailsElement> | undefined | null;
'on:toggle'?: ToggleEventHandler<HTMLDetailsElement> | undefined | null;
ontoggle?: ToggleEventHandler<HTMLDetailsElement> | undefined | null;
ontogglecapture?: ToggleEventHandler<HTMLDetailsElement> | undefined | null;
}
export interface HTMLDelAttributes extends HTMLAttributes<HTMLModElement> {

@ -16,7 +16,7 @@
## declaration_duplicate_module_import
> Cannot declare a variable with the same name as an import inside `<script module>`
> Cannot declare a variable with the same name as an import from `<script module>`
## derived_invalid_export

@ -53,6 +53,10 @@ This error occurs when using `hydratable` multiple times with the same key. To a
> `csp.nonce` was set while `csp.hash` was `true`. These options cannot be used simultaneously.
## invalid_id_prefix
> The `idPrefix` option cannot include `--`.
## lifecycle_function_unavailable
> `%name%(...)` is not available on the server

@ -2,7 +2,7 @@
"name": "svelte",
"description": "Cybernetically enhanced web apps",
"license": "MIT",
"version": "5.54.0",
"version": "5.55.1",
"type": "module",
"types": "./types/index.d.ts",
"engines": {
@ -178,7 +178,7 @@
"clsx": "^2.1.1",
"devalue": "^5.6.4",
"esm-env": "^1.2.1",
"esrap": "^2.2.2",
"esrap": "^2.2.4",
"is-reference": "^3.0.3",
"locate-character": "^3.0.0",
"magic-string": "^0.30.11",

@ -117,12 +117,12 @@ export function declaration_duplicate(node, name) {
}
/**
* Cannot declare a variable with the same name as an import inside `<script module>`
* Cannot declare a variable with the same name as an import from `<script module>`
* @param {null | number | NodeLike} node
* @returns {never}
*/
export function declaration_duplicate_module_import(node) {
e(node, 'declaration_duplicate_module_import', `Cannot declare a variable with the same name as an import inside \`<script module>\`\nhttps://svelte.dev/e/declaration_duplicate_module_import`);
e(node, 'declaration_duplicate_module_import', `Cannot declare a variable with the same name as an import from \`<script module>\`\nhttps://svelte.dev/e/declaration_duplicate_module_import`);
}
/**

@ -138,11 +138,13 @@ const visitors = {
delete node.abstract;
delete node.implements;
delete node.superTypeArguments;
delete node.superTypeParameters;
return context.next();
},
ClassExpression(node, context) {
delete node.implements;
delete node.superTypeArguments;
delete node.superTypeParameters;
return context.next();
},
MethodDefinition(node, context) {

@ -1074,6 +1074,9 @@ function calculate_blockers(instance, analysis) {
let awaited = false;
/** @type {Array<ESTree.Statement | ESTree.VariableDeclarator>} */
let sync_group = [];
// TODO this should probably be attached to the scope?
const promises = b.id('$$promises');
@ -1088,6 +1091,13 @@ function calculate_blockers(instance, analysis) {
binding.blocker = blocker;
}
function flush_sync_group() {
if (sync_group.length === 0) return;
analysis.instance_body.async.push({ nodes: sync_group, has_await: false });
sync_group = [];
}
/**
* Analysis of blockers for functions is deferred until we know which statements are async/blockers
* @type {Array<ESTree.FunctionDeclaration | ESTree.VariableDeclarator>}
@ -1149,6 +1159,9 @@ function calculate_blockers(instance, analysis) {
trace_references(declarator, reads, writes, instance.scope);
// Needs to happen before blocker computation
if (has_await) flush_sync_group();
const blocker = /** @type {NonNullable<Binding['blocker']>} */ (
b.member(promises, b.literal(analysis.instance_body.async.length), true)
);
@ -1161,11 +1174,12 @@ function calculate_blockers(instance, analysis) {
push_declaration(id, blocker);
}
// one declarator per declaration, makes things simpler
analysis.instance_body.async.push({
node: declarator,
has_await
});
if (has_await) {
// one declarator per declaration, makes things simpler
analysis.instance_body.async.push({ nodes: [declarator], has_await: true });
} else {
sync_group.push(declarator);
}
}
}
} else if (awaited) {
@ -1177,6 +1191,9 @@ function calculate_blockers(instance, analysis) {
trace_references(node, reads, writes, instance.scope);
// Needs to happen before blocker computation
if (has_await) flush_sync_group();
const blocker = /** @type {NonNullable<Binding['blocker']>} */ (
b.member(promises, b.literal(analysis.instance_body.async.length), true)
);
@ -1187,15 +1204,20 @@ function calculate_blockers(instance, analysis) {
if (node.type === 'ClassDeclaration') {
push_declaration(node.id, blocker);
analysis.instance_body.async.push({ node, has_await });
}
if (has_await) {
analysis.instance_body.async.push({ nodes: [node], has_await: true });
} else {
analysis.instance_body.async.push({ node, has_await });
sync_group.push(node);
}
} else {
analysis.instance_body.sync.push(node);
}
}
flush_sync_group();
for (const fn of functions) {
/** @type {Set<Binding>} */
const reads_writes = new Set();

@ -10,7 +10,7 @@ export function visit_function(node, context) {
for (const [name] of context.state.scope.references) {
const binding = context.state.scope.get(name);
if (binding && binding.scope.function_depth < context.state.scope.function_depth) {
if (binding && binding.scope !== context.state.scope) {
context.state.expression.references.add(binding);
}
}

@ -161,7 +161,6 @@ export function ensure_no_module_import_conflict(node, state) {
state.scope === state.analysis.instance.scope &&
state.analysis.module.scope.get(id.name)?.declaration_kind === 'import'
) {
// TODO fix the message here
e.declaration_duplicate_module_import(node.id);
}
}

@ -352,7 +352,7 @@ export function client_component(analysis, options) {
)
);
} else if (dev) {
component_returned_object.push(b.spread(b.call(b.id('$.legacy_api'))));
component_returned_object.unshift(b.spread(b.call(b.id('$.legacy_api'))));
}
const push_args = [b.id('$$props'), b.literal(analysis.runes)];

@ -101,7 +101,7 @@ export function process_children(nodes, initial, is_element, context) {
if (is_static_element(node)) {
skipped += 1;
} else if (
node.type === 'EachBlock' &&
(node.type === 'EachBlock' || node.type === 'HtmlTag') &&
nodes.length === 1 &&
is_element &&
// In case it's wrapped in async the async logic will want to skip sibling nodes up until the end, hence we cannot make this controlled
@ -109,8 +109,6 @@ export function process_children(nodes, initial, is_element, context) {
!node.metadata.expression.is_async()
) {
node.metadata.is_controlled = true;
} else if (node.type === 'HtmlTag' && nodes.length === 1 && is_element) {
node.metadata.is_controlled = true;
} else {
const id = flush_node(
false,

@ -47,64 +47,24 @@ export function transform_body(instance_body, runner, transform) {
// Thunks for the await expressions
if (instance_body.async.length > 0) {
const thunks = instance_body.async.map((s) => {
if (s.node.type === 'VariableDeclarator') {
const visited = /** @type {ESTree.VariableDeclaration | ESTree.EmptyStatement} */ (
transform(b.var(s.node.id, s.node.init))
);
const statements =
visited.type === 'VariableDeclaration'
? visited.declarations.map((node) => {
if (
node.id.type === 'Identifier' &&
(node.id.name.startsWith('$$d') || node.id.name.startsWith('$$array'))
) {
// this is an intermediate declaration created in VariableDeclaration.js;
// subsequent statements depend on it
return b.var(node.id, node.init);
}
return b.stmt(b.assignment('=', node.id, node.init ?? b.void0));
})
: [];
if (statements.length === 1) {
const statement = /** @type {ESTree.ExpressionStatement} */ (statements[0]);
return b.thunk(statement.expression, s.has_await);
}
return b.thunk(b.block(statements), s.has_await);
}
const thunks = instance_body.async.map((entry) => {
/** @type {ESTree.Statement[]} */
const entry_statements = [];
if (s.node.type === 'ClassDeclaration') {
return b.thunk(
b.assignment(
'=',
s.node.id,
/** @type {ESTree.ClassExpression} */ ({ ...s.node, type: 'ClassExpression' })
),
s.has_await
);
for (const node of entry.nodes) {
entry_statements.push(...transform_async_node(node, transform));
}
if (s.node.type === 'ExpressionStatement') {
// the expression may be a $inspect call, which will be transformed into an empty statement
const expression = /** @type {ESTree.Expression | ESTree.EmptyStatement} */ (
transform(s.node.expression)
);
if (expression.type === 'EmptyStatement') {
// Keep indices stable for async sequencing while avoiding array holes in run([...]).
return b.thunk(b.void0, false);
}
if (entry_statements.length === 0) {
// Keep indices stable for async sequencing while avoiding array holes in run([...]).
return b.thunk(b.void0, false);
}
return expression.type === 'AwaitExpression'
? b.thunk(expression, true)
: b.thunk(b.unary('void', expression), s.has_await);
if (entry_statements.length === 1 && entry_statements[0].type === 'ExpressionStatement') {
return b.thunk(entry_statements[0].expression, entry.has_await);
}
return b.thunk(b.block([/** @type {ESTree.Statement} */ (transform(s.node))]), s.has_await);
return b.thunk(b.block(entry_statements), entry.has_await);
});
// TODO get the `$$promises` ID from scope
@ -113,3 +73,63 @@ export function transform_body(instance_body, runner, transform) {
return statements;
}
/**
* @param {ESTree.Statement | ESTree.VariableDeclarator} node
* @param {(node: ESTree.Node) => ESTree.Node} transform
* @returns {ESTree.Statement[]}
*/
function transform_async_node(node, transform) {
if (node.type === 'VariableDeclarator') {
const visited = /** @type {ESTree.VariableDeclaration | ESTree.EmptyStatement} */ (
transform(b.var(node.id, node.init))
);
return visited.type === 'VariableDeclaration'
? visited.declarations.map((node) => {
if (
node.id.type === 'Identifier' &&
(node.id.name.startsWith('$$d') || node.id.name.startsWith('$$array'))
) {
// This intermediate declaration is created in VariableDeclaration.js;
// subsequent statements may depend on it.
return b.var(node.id, node.init);
}
return b.stmt(b.assignment('=', node.id, node.init ?? b.void0));
})
: [];
}
if (node.type === 'ClassDeclaration') {
return [
b.stmt(
b.assignment(
'=',
node.id,
/** @type {ESTree.ClassExpression} */ ({ ...node, type: 'ClassExpression' })
)
)
];
}
if (node.type === 'ExpressionStatement') {
// The expression may be a $inspect call, which will be transformed into an empty statement.
const expression = /** @type {ESTree.Expression | ESTree.EmptyStatement} */ (
transform(node.expression)
);
if (expression.type === 'EmptyStatement') {
return [];
}
if (expression.type === 'AwaitExpression') {
return [b.stmt(expression)];
}
return [b.stmt(b.unary('void', expression))];
}
const statement = /** @type {ESTree.Statement | ESTree.EmptyStatement} */ (transform(node));
return statement.type === 'EmptyStatement' ? [] : [statement];
}

@ -131,7 +131,7 @@ export interface ComponentAnalysis extends Analysis {
instance_body: {
hoisted: Array<Statement | ModuleDeclaration>;
sync: Array<Statement | ModuleDeclaration | VariableDeclaration>;
async: Array<{ node: Statement | VariableDeclarator; has_await: boolean }>;
async: Array<{ nodes: Array<Statement | VariableDeclarator>; has_await: boolean }>;
declarations: Array<Identifier>;
};
}

@ -592,8 +592,13 @@ const svelte_visitors = (comments) => ({
},
ConstTag(node, context) {
context.write('{@');
context.visit(node.declaration);
context.write('{@const ');
const declarators = node.declaration.declarations;
for (let i = 0; i < declarators.length; i++) {
if (i > 0) context.write(', ');
context.visit(declarators[i]);
}
context.write('}');
},

@ -5,9 +5,7 @@ import { hydrate_node, hydrating } from '../dom/hydration.js';
import { block, branch, destroy_effect } from '../reactivity/effects.js';
import { set, source } from '../reactivity/sources.js';
import { set_should_intro } from '../render.js';
import { get } from '../runtime.js';
import { assign_nodes } from '../dom/template.js';
import { create_comment } from '../dom/operations.js';
import { active_effect, get } from '../runtime.js';
/**
* @template {(anchor: Comment, props: any) => any} Component
@ -29,13 +27,6 @@ export function hmr(fn) {
let ran = false;
// Surround the wrapped effects with comments and assign the nodes
// on the wrapping effects so the parent can properly do DOM operations.
let start = create_comment();
let end = create_comment();
anchor.before(start);
block(() => {
if (component === (component = get(current))) {
return;
@ -52,16 +43,21 @@ export function hmr(fn) {
if (ran) set_should_intro(false);
// preserve getters/setters
Object.defineProperties(
instance,
Object.getOwnPropertyDescriptors(
// @ts-expect-error
new.target ? new component(anchor, props) : component(anchor, props)
)
);
var result =
// @ts-expect-error
new.target ? new component(anchor, props) : component(anchor, props);
// a component is not guaranteed to return something and we can't invoke getOwnPropertyDescriptors on undefined
if (result) {
Object.defineProperties(instance, Object.getOwnPropertyDescriptors(result));
}
if (ran) set_should_intro(true);
});
// Forward the nodes from the inner effect to the outer active effect which would
// get them if the HMR wrapper wasn't there. Do this inside the block not outside
// so that HMR updates to the component will also update the nodes on the active effect.
/** @type {Effect} */ (active_effect).nodes = effect.nodes;
}, EFFECT_TRANSPARENT);
ran = true;
@ -70,10 +66,6 @@ export function hmr(fn) {
anchor = hydrate_node;
}
anchor.before(end);
assign_nodes(start, end);
return instance;
}

@ -42,6 +42,7 @@ import { DEV } from 'esm-env';
import { derived_safe_equal } from '../../reactivity/deriveds.js';
import { current_batch } from '../../reactivity/batch.js';
import * as e from '../../errors.js';
import { tag } from '../../dev/tracing.js';
// When making substantive changes to this file, validate them with the each block stress test:
// https://svelte.dev/playground/1972b2cf46564476ad8c8c6405b23b7b
@ -205,6 +206,10 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f
return is_array(collection) ? collection : collection == null ? [] : array_from(collection);
});
if (DEV) {
tag(each_array, '{#each ...}');
}
/** @type {V[]} */
var array;

@ -209,8 +209,8 @@ export async function* for_await_track_reactivity_loss(iterable) {
yield value;
}
} finally {
// If the iterator had a normal completion and `return` is defined on the iterator, call it and return the value
if (normal_completion && iterator.return !== undefined) {
// If the iterator had an abrupt completion and `return` is defined on the iterator, call it and return the value
if (!normal_completion && iterator.return !== undefined) {
// eslint-disable-next-line no-unsafe-finally
return /** @type {TReturn} */ ((await track_reactivity_loss(iterator.return()))().value);
}
@ -307,15 +307,16 @@ export function wait(blockers) {
* @returns {(skip?: boolean) => void}
*/
export function increment_pending() {
var boundary = /** @type {Boundary} */ (/** @type {Effect} */ (active_effect).b);
var effect = /** @type {Effect} */ (active_effect);
var boundary = /** @type {Boundary} */ (effect.b);
var batch = /** @type {Batch} */ (current_batch);
var blocking = boundary.is_rendered();
boundary.update_pending_count(1, batch);
batch.increment(blocking);
batch.increment(blocking, effect);
return (skip = false) => {
boundary.update_pending_count(-1, batch);
batch.decrement(blocking, skip);
batch.decrement(blocking, effect, skip);
};
}

@ -90,20 +90,20 @@ var source_stacks = DEV ? new Set() : null;
let uid = 1;
export class Batch {
// for debugging. TODO remove once async is stable
id = uid++;
/**
* The current values of any sources that are updated in this batch
* The current values of any signals that are updated in this batch.
* Tuple format: [value, is_derived] (note: is_derived is false for deriveds, too, if they were overridden via assignment)
* They keys of this map are identical to `this.#previous`
* @type {Map<Source, any>}
* @type {Map<Value, [any, boolean]>}
*/
current = new Map();
/**
* The values of any sources that are updated in this batch _before_ those updates took place.
* The values of any signals (sources and deriveds) that are updated in this batch _before_ those updates took place.
* They keys of this map are identical to `this.#current`
* @type {Map<Source, any>}
* @type {Map<Value, any>}
*/
previous = new Map();
@ -121,14 +121,16 @@ export class Batch {
#discard_callbacks = new Set();
/**
* The number of async effects that are currently in flight
* Async effects that are currently in flight
* @type {Map<Effect, number>}
*/
#pending = 0;
#pending = new Map();
/**
* The number of async effects that are currently in flight, _not_ inside a pending boundary
* Async effects that are currently in flight, _not_ inside a pending boundary
* @type {Map<Effect, number>}
*/
#blocking_pending = 0;
#blocking_pending = new Map();
/**
* A deferred that resolves when the batch is committed, used with `settled()`
@ -143,6 +145,12 @@ export class Batch {
*/
#roots = [];
/**
* Effects created while this batch was active.
* @type {Effect[]}
*/
#new_effects = [];
/**
* Deferred effects (which run after async work has completed) that are DIRTY
* @type {Set<Effect>}
@ -164,12 +172,45 @@ 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<Effect>}
*/
#unskipped_branches = new Set();
is_fork = false;
#decrement_queued = false;
/** @type {Set<Batch>} */
#blockers = new Set();
#is_deferred() {
return this.is_fork || this.#blocking_pending > 0;
return this.is_fork || this.#blocking_pending.size > 0;
}
#is_blocked() {
for (const batch of this.#blockers) {
for (const effect of batch.#blocking_pending.keys()) {
var skipped = false;
var e = effect;
while (e.parent !== null) {
if (this.#skipped_branches.has(e)) {
skipped = true;
break;
}
e = e.parent;
}
if (!skipped) {
return true;
}
}
}
return false;
}
/**
@ -180,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() {
@ -264,7 +308,7 @@ export class Batch {
collected_effects = null;
legacy_updates = null;
if (this.#is_deferred()) {
if (this.#is_deferred() || this.#is_blocked()) {
this.#defer_effects(render_effects);
this.#defer_effects(effects);
@ -272,7 +316,7 @@ export class Batch {
reset_branch(e, t);
}
} else {
if (this.#pending === 0) {
if (this.#pending.size === 0) {
batches.delete(this);
}
@ -314,7 +358,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();
}
}
@ -383,18 +429,23 @@ export class Batch {
/**
* Associate a change to a given source with the current
* batch, noting its previous and current values
* @param {Source} source
* @param {any} old_value
* @param {Value} source
* @param {any} value
* @param {boolean} [is_derived]
*/
capture(source, old_value) {
if (old_value !== UNINITIALIZED && !this.previous.has(source)) {
this.previous.set(source, old_value);
capture(source, value, is_derived = false) {
if (source.v !== UNINITIALIZED && !this.previous.has(source)) {
this.previous.set(source, source.v);
}
// Don't save errors in `batch_values`, or they won't be thrown in `runtime.js#get`
if ((source.f & ERROR_VALUE) === 0) {
this.current.set(source, source.v);
batch_values?.set(source, source.v);
this.current.set(source, [value, is_derived]);
batch_values?.set(source, value);
}
if (!this.is_fork) {
source.v = value;
}
}
@ -442,6 +493,13 @@ export class Batch {
batches.delete(this);
}
/**
* @param {Effect} effect
*/
register_created_effect(effect) {
this.#new_effects.push(effect);
}
#commit() {
// If there are other pending batches, they now need to be 'rebased' —
// in other words, we re-run block/async effects with the newly
@ -453,11 +511,13 @@ export class Batch {
/** @type {Source[]} */
var sources = [];
for (const [source, value] of this.current) {
for (const [source, [value, is_derived]] of this.current) {
if (batch.current.has(source)) {
if (is_earlier && value !== batch.current.get(source)) {
var batch_value = /** @type {[any, boolean]} */ (batch.current.get(source))[0]; // faster than destructuring
if (is_earlier && value !== batch_value) {
// bring the value up to date
batch.current.set(source, value);
batch.current.set(source, [value, is_derived]);
} else {
// same value or later batch has more recent value,
// no need to re-run these effects
@ -481,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<Value>} */
@ -493,6 +566,25 @@ export class Batch {
mark_effects(source, others, marked, checked);
}
checked = new Map();
var current_unequal = [...batch.current.keys()].filter((c) =>
this.current.has(c) ? /** @type {[any, boolean]} */ (this.current.get(c))[0] !== c : true
);
for (const effect of this.#new_effects) {
if (
(effect.f & (DESTROYED | INERT | EAGER_EFFECT)) === 0 &&
depends_on(effect, current_unequal, checked)
) {
if ((effect.f & (ASYNC | BLOCK_EFFECT)) !== 0) {
set_signal_status(effect, DIRTY);
batch.schedule(effect);
} else {
batch.#dirty_effects.add(effect);
}
}
}
// Only apply and traverse when we know we triggered async work with marking the effects
if (batch.#roots.length > 0) {
batch.apply();
@ -507,24 +599,56 @@ export class Batch {
batch.deactivate();
}
}
for (const batch of batches) {
if (batch.#blockers.has(this)) {
batch.#blockers.delete(this);
if (batch.#blockers.size === 0 && !batch.#is_deferred()) {
batch.activate();
batch.#process();
}
}
}
}
/**
*
* @param {boolean} blocking
* @param {Effect} effect
*/
increment(blocking) {
this.#pending += 1;
if (blocking) this.#blocking_pending += 1;
increment(blocking, effect) {
let pending_count = this.#pending.get(effect) ?? 0;
this.#pending.set(effect, pending_count + 1);
if (blocking) {
let blocking_pending_count = this.#blocking_pending.get(effect) ?? 0;
this.#blocking_pending.set(effect, blocking_pending_count + 1);
}
}
/**
* @param {boolean} blocking
* @param {Effect} effect
* @param {boolean} skip - whether to skip updates (because this is triggered by a stale reaction)
*/
decrement(blocking, skip) {
this.#pending -= 1;
if (blocking) this.#blocking_pending -= 1;
decrement(blocking, effect, skip) {
let pending_count = this.#pending.get(effect) ?? 0;
if (pending_count === 1) {
this.#pending.delete(effect);
} else {
this.#pending.set(effect, pending_count - 1);
}
if (blocking) {
let blocking_pending_count = this.#blocking_pending.get(effect) ?? 0;
if (blocking_pending_count === 1) {
this.#blocking_pending.delete(effect);
} else {
this.#blocking_pending.set(effect, blocking_pending_count - 1);
}
}
if (this.#decrement_queued || skip) return;
this.#decrement_queued = true;
@ -597,15 +721,37 @@ export class Batch {
// if there are multiple batches, we are 'time travelling' —
// we need to override values with the ones in this batch...
batch_values = new Map(this.current);
batch_values = new Map();
for (const [source, [value]] of this.current) {
batch_values.set(source, value);
}
// ...and undo changes belonging to other batches
// ...and undo changes belonging to other batches unless they block this one
for (const batch of batches) {
if (batch === this || batch.is_fork) continue;
for (const [source, previous] of batch.previous) {
if (!batch_values.has(source)) {
batch_values.set(source, previous);
// A batch is blocked on an earlier batch if it overlaps with the earlier batch's changes but is not a superset
var intersects = false;
var differs = false;
if (batch.id < this.id) {
for (const [source, [, is_derived]] of batch.current) {
// Derived values don't partake in the blocking mechanism, because a derived could
// be triggered in one batch already but not the other one yet, causing a false-positive
if (is_derived) continue;
intersects ||= this.current.has(source);
differs ||= !this.current.has(source);
}
}
if (intersects && differs) {
this.#blockers.add(batch);
} else {
for (const [source, previous] of batch.previous) {
if (!batch_values.has(source)) {
batch_values.set(source, previous);
}
}
}
}
@ -669,6 +815,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.
@ -1044,11 +1191,6 @@ export function fork(fn) {
flushSync(fn);
// revert state changes
for (var [source, value] of batch.previous) {
source.v = value;
}
return {
commit: async () => {
if (committed) {
@ -1065,7 +1207,7 @@ export function fork(fn) {
batch.is_fork = false;
// apply changes and update write versions so deriveds see the change
for (var [source, value] of batch.current) {
for (var [source, [value]] of batch.current) {
source.v = value;
source.wv = increment_write_version();
}

@ -384,7 +384,6 @@ export function execute_derived(derived) {
* @returns {void}
*/
export function update_derived(derived) {
var old_value = derived.v;
var value = execute_derived(derived);
if (!derived.equals(value)) {
@ -395,8 +394,11 @@ export function update_derived(derived) {
// otherwise, the next time we get here after a 'real world' state
// change, `derived.equals` may incorrectly return `true`
if (!current_batch?.is_fork || derived.deps === null) {
derived.v = value;
current_batch?.capture(derived, old_value);
if (current_batch !== null) {
current_batch.capture(derived, value, true);
} else {
derived.v = value;
}
// deriveds without dependencies should never be recomputed
if (derived.deps === null) {

@ -42,7 +42,7 @@ import { DEV } from 'esm-env';
import { define_property } from '../../shared/utils.js';
import { get_next_sibling } from '../dom/operations.js';
import { component_context, dev_current_component_function, dev_stack } from '../context.js';
import { Batch, collected_effects } from './batch.js';
import { Batch, collected_effects, current_batch } from './batch.js';
import { flatten, increment_pending } from './async.js';
import { without_reactive_context } from '../dom/elements/bindings/shared.js';
import { set_signal_status } from './status.js';
@ -120,6 +120,8 @@ function create_effect(type, fn) {
effect.component_function = dev_current_component_function;
}
current_batch?.register_created_effect(effect);
/** @type {Effect | null} */
var e = effect;
@ -559,6 +561,7 @@ export function destroy_effect(effect, remove_dom = true) {
effect.fn =
effect.nodes =
effect.ac =
effect.b =
null;
}

@ -180,18 +180,10 @@ export function set(source, value, should_proxy = false) {
*/
export function internal_set(source, value, updated_during_traversal = null) {
if (!source.equals(value)) {
var old_value = source.v;
if (is_destroying_effect) {
old_values.set(source, value);
} else {
old_values.set(source, old_value);
}
source.v = value;
old_values.set(source, is_destroying_effect ? value : source.v);
var batch = Batch.ensure();
batch.capture(source, old_value);
batch.capture(source, value);
if (DEV) {
if (tracing_mode_flag || active_effect !== null) {

@ -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);

@ -105,6 +105,18 @@ export function invalid_csp() {
throw error;
}
/**
* The `idPrefix` option cannot include `--`.
* @returns {never}
*/
export function invalid_id_prefix() {
const error = new Error(`invalid_id_prefix\nThe \`idPrefix\` option cannot include \`--\`.\nhttps://svelte.dev/e/invalid_id_prefix`);
error.name = 'Svelte error';
throw error;
}
/**
* `%name%(...)` is not available on the server
* @param {string} name

@ -468,10 +468,14 @@ export class Renderer {
}
this.local = other.local;
this.#out = other.#out.map((item) => {
if (item instanceof Renderer) {
item.subsume(item);
this.#out = other.#out.map((item, i) => {
const current = this.#out[i];
if (current instanceof Renderer && item instanceof Renderer) {
current.subsume(item);
return current;
}
return item;
});
this.promise = other.promise;
@ -755,6 +759,10 @@ export class Renderer {
* @returns {Renderer}
*/
static #open_render(mode, component, options) {
if (options.idPrefix?.includes('--')) {
e.invalid_id_prefix();
}
var previous_context = ssr_context;
try {

@ -8,37 +8,3 @@ export interface TickContext {
};
settled: boolean;
}
export interface SpringOpts {
stiffness?: number;
damping?: number;
precision?: number;
}
export interface SpringUpdateOpts {
/**
* @deprecated Only use this for the spring store; does nothing when set on the Spring class
*/
hard?: any;
/**
* @deprecated Only use this for the spring store; does nothing when set on the Spring class
*/
soft?: string | number | boolean;
/**
* Only use this for the Spring class; does nothing when set on the spring store
*/
instant?: boolean;
/**
* Only use this for the Spring class; does nothing when set on the spring store
*/
preserveMomentum?: number;
}
export type Updater<T> = (target_value: T, value: T) => T;
export interface TweenedOptions<T> {
delay?: number;
duration?: number | ((from: T, to: T) => number);
easing?: (t: number) => number;
interpolate?: (a: T, b: T) => (t: number) => T;
}

@ -1,16 +1,49 @@
import { Readable, type Unsubscriber } from '../store/public.js';
import { SpringUpdateOpts, TweenedOptions, Updater, SpringOpts } from './private.js';
export interface SpringOptions {
stiffness?: number;
damping?: number;
precision?: number;
}
export interface SpringUpdateOptions {
/**
* @deprecated Only use this for the spring store; does nothing when set on the Spring class
*/
hard?: any;
/**
* @deprecated Only use this for the spring store; does nothing when set on the Spring class
*/
soft?: string | number | boolean;
/**
* Only use this for the Spring class; does nothing when set on the spring store
*/
instant?: boolean;
/**
* Only use this for the Spring class; does nothing when set on the spring store
*/
preserveMomentum?: number;
}
export type Updater<T> = (target_value: T, value: T) => T;
export interface TweenOptions<T> {
delay?: number;
duration?: number | ((from: T, to: T) => number);
easing?: (t: number) => number;
interpolate?: (a: T, b: T) => (t: number) => T;
}
// TODO we do declaration merging here in order to not have a breaking change (renaming the Spring interface)
// this means both the Spring class and the Spring interface are merged into one with some things only
// existing on one side. In Svelte 6, remove the type definition and move the jsdoc onto the class in spring.js
export interface Spring<T> extends Readable<T> {
set(new_value: T, opts?: SpringUpdateOpts): Promise<void>;
set(new_value: T, opts?: SpringUpdateOptions): Promise<void>;
/**
* @deprecated Only exists on the legacy `spring` store, not the `Spring` class
*/
update: (fn: Updater<T>, opts?: SpringUpdateOpts) => Promise<void>;
update: (fn: Updater<T>, opts?: SpringUpdateOptions) => Promise<void>;
/**
* @deprecated Only exists on the legacy `spring` store, not the `Spring` class
*/
@ -37,7 +70,7 @@ export interface Spring<T> extends Readable<T> {
* @since 5.8.0
*/
export class Spring<T> {
constructor(value: T, options?: SpringOpts);
constructor(value: T, options?: SpringOptions);
/**
* Create a spring whose value is bound to the return value of `fn`. This must be called
@ -53,7 +86,7 @@ export class Spring<T> {
* </script>
* ```
*/
static of<U>(fn: () => U, options?: SpringOpts): Spring<U>;
static of<U>(fn: () => U, options?: SpringOptions): Spring<U>;
/**
* Sets `spring.target` to `value` and returns a `Promise` that resolves if and when `spring.current` catches up to it.
@ -63,7 +96,7 @@ export class Spring<T> {
* If `options.preserveMomentum` is provided, the spring will continue on its current trajectory for
* the specified number of milliseconds. This is useful for things like 'fling' gestures.
*/
set(value: T, options?: SpringUpdateOpts): Promise<void>;
set(value: T, options?: SpringUpdateOptions): Promise<void>;
damping: number;
precision: number;
@ -81,8 +114,8 @@ export class Spring<T> {
}
export interface Tweened<T> extends Readable<T> {
set(value: T, opts?: TweenedOptions<T>): Promise<void>;
update(updater: Updater<T>, opts?: TweenedOptions<T>): Promise<void>;
set(value: T, opts?: TweenOptions<T>): Promise<void>;
update(updater: Updater<T>, opts?: TweenOptions<T>): Promise<void>;
}
export { prefersReducedMotion, spring, tweened, Tween } from './index.js';

@ -1,6 +1,6 @@
/** @import { Task } from '#client' */
/** @import { SpringOpts, SpringUpdateOpts, TickContext } from './private.js' */
/** @import { Spring as SpringStore } from './public.js' */
/** @import { TickContext } from './private.js' */
/** @import { Spring as SpringStore, SpringOptions, SpringUpdateOptions } from './public.js' */
import { writable } from '../store/shared/index.js';
import { loop } from '../internal/client/loop.js';
import { raf } from '../internal/client/timing.js';
@ -62,7 +62,7 @@ function tick_spring(ctx, last_value, current_value, target_value) {
* @deprecated Use [`Spring`](https://svelte.dev/docs/svelte/svelte-motion#Spring) instead
* @template [T=any]
* @param {T} [value]
* @param {SpringOpts} [opts]
* @param {SpringOptions} [opts]
* @returns {SpringStore<T>}
*/
export function spring(value, opts = {}) {
@ -83,7 +83,7 @@ export function spring(value, opts = {}) {
let cancel_task = false;
/**
* @param {T} new_value
* @param {SpringUpdateOpts} opts
* @param {SpringUpdateOptions} opts
* @returns {Promise<void>}
*/
function set(new_value, opts = {}) {
@ -191,7 +191,7 @@ export class Spring {
/**
* @param {T} value
* @param {SpringOpts} [options]
* @param {SpringOptions} [options]
*/
constructor(value, options = {}) {
this.#current = DEV ? tag(state(value), 'Spring.current') : state(value);
@ -225,7 +225,7 @@ export class Spring {
* ```
* @template U
* @param {() => U} fn
* @param {SpringOpts} [options]
* @param {SpringOptions} [options]
*/
static of(fn, options) {
const spring = new Spring(fn(), options);
@ -293,7 +293,7 @@ export class Spring {
* the specified number of milliseconds. This is useful for things like 'fling' gestures.
*
* @param {T} value
* @param {SpringUpdateOpts} [options]
* @param {SpringUpdateOptions} [options]
*/
set(value, options) {
this.#deferred?.reject(new Error('Aborted'));

@ -1,6 +1,5 @@
/** @import { Task } from '../internal/client/types' */
/** @import { Tweened } from './public' */
/** @import { TweenedOptions } from './private' */
/** @import { Tweened, TweenOptions } from './public' */
import { writable } from '../store/shared/index.js';
import { raf } from '../internal/client/timing.js';
import { loop } from '../internal/client/loop.js';
@ -84,7 +83,7 @@ function get_interpolator(a, b) {
* @deprecated Use [`Tween`](https://svelte.dev/docs/svelte/svelte-motion#Tween) instead
* @template T
* @param {T} [value]
* @param {TweenedOptions<T>} [defaults]
* @param {TweenOptions<T>} [defaults]
* @returns {Tweened<T>}
*/
export function tweened(value, defaults = {}) {
@ -94,7 +93,7 @@ export function tweened(value, defaults = {}) {
let target_value = value;
/**
* @param {T} new_value
* @param {TweenedOptions<T>} [opts]
* @param {TweenOptions<T>} [opts]
*/
function set(new_value, opts) {
target_value = new_value;
@ -180,7 +179,7 @@ export class Tween {
#current;
#target;
/** @type {TweenedOptions<T>} */
/** @type {TweenOptions<T>} */
#defaults;
/** @type {import('../internal/client/types').Task | null} */
@ -188,7 +187,7 @@ export class Tween {
/**
* @param {T} value
* @param {TweenedOptions<T>} options
* @param {TweenOptions<T>} options
*/
constructor(value, options = {}) {
this.#current = state(value);
@ -216,7 +215,7 @@ export class Tween {
* ```
* @template U
* @param {() => U} fn
* @param {TweenedOptions<U>} [options]
* @param {TweenOptions<U>} [options]
*/
static of(fn, options) {
const tween = new Tween(fn(), options);
@ -233,7 +232,7 @@ export class Tween {
*
* If `options` are provided, they will override the tween's defaults.
* @param {T} value
* @param {TweenedOptions<T>} [options]
* @param {TweenOptions<T>} [options]
* @returns
*/
set(value, options) {

@ -95,6 +95,10 @@ export class SvelteDate extends Date {
return date_proto[method].apply(this, args);
});
if (DEV) {
tag(d, `SvelteDate.${method}()`);
}
this.#deriveds.set(method, d);
set_active_reaction(reaction);

@ -4,5 +4,5 @@
* The current version, as set in package.json.
* @type {string}
*/
export const VERSION = '5.54.0';
export const VERSION = '5.55.1';
export const PUBLIC_VERSION = '5';

@ -0,0 +1,7 @@
<h1>Hello</h1>
<style>
h1 {
color: var(--color);
}
</style>

@ -0,0 +1,7 @@
import { test } from '../../test';
export default test({
compileOptions: {
hmr: true
}
});

@ -0,0 +1,5 @@
<script>
import Component from "./Component.svelte";
</script>
<Component --color="red" />

@ -3,6 +3,6 @@
</script>
{#each boxes as box}
{@const area = box.width * box.height;}
{@const area = box.width * box.height}
{box.width} * {box.height} = {area}
{/each}

@ -0,0 +1,34 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
html: `
<input>
<p>hello</p>
<p>hello</p>
`,
ssrHtml: `
<input value="hello">
<p>hello</p>
<p>hello</p>
`,
async test({ assert, target }) {
const [input] = target.querySelectorAll('input');
flushSync(() => {
input.value = 'goodbye';
input.dispatchEvent(new InputEvent('input', { bubbles: true }));
});
assert.htmlEqual(
target.innerHTML,
`
<input>
<p>goodbye</p>
<p>goodbye</p>
`
);
}
});

@ -0,0 +1,15 @@
<svelte:options runes={false} />
<script>
let message = 'hello';
</script>
<input bind:value={message} />
{#if true}
{@const m1 = message}
{@const m2 = (() => m1)()}
<p>{m1}</p>
<p>{m2}</p>
{/if}

@ -0,0 +1,8 @@
<svelte:options customElement={{
tag: "my-inner",
props: { value: { reflect: true }}
}} />
<script>
export let value;
</script>
{value}

@ -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,
`
<button>inc</button>
<my-inner value="2"></my-inner>
2
`
);
}
});

@ -0,0 +1,12 @@
<script>
import "./Inner.svelte"
let count = 1;
</script>
<button on:click={() => count++}>inc</button>
<!-- updating value prop will cause a flushSync -->
<my-inner value={count}></my-inner>
<!-- updating count will cause an internal_set -->
{#each [count] as row}
{row}
{/each}

@ -0,0 +1,29 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target }) {
await tick();
const [a, b, resolve] = target.querySelectorAll('button');
a.click();
await tick();
b.click();
await tick();
resolve.click();
await tick();
resolve.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>a</button>
<button>b</button>
<button>resolve</button>
hi
1
`
);
}
});

@ -0,0 +1,21 @@
<script>
let a = $state(0);
let b = $state(0);
let a_b = $derived(a * b);
const queued = [];
function push(value) {
if (!value) return value;
return new Promise(resolve => {
queued.push(() => resolve(value));
});
}
</script>
<button onclick={() => (a++)}>a</button>
<button onclick={() => (b++)}>b</button>
<button onclick={() => (queued.shift()?.())}>resolve</button>
<!-- a_b called in a block effect before being called in an async effect -->
{#if a_b}hi{/if}
{await push(a_b)}

@ -1,102 +0,0 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
skip: true,
async test({ assert, target }) {
const [add, shift] = target.querySelectorAll('button');
add.click();
await tick();
add.click();
await tick();
add.click();
await tick();
// TODO pending count / number of pushes is off
assert.htmlEqual(
target.innerHTML,
`
<button>add</button>
<button>shift</button>
<p>pending=6 values.length=1 values=[1]</p>
<div>not keyed:
<div>1</div>
</div>
<div>keyed:
<div>1</div>
</div>
`
);
shift.click();
await tick();
shift.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>add</button>
<button>shift</button>
<p>pending=4 values.length=2 values=[1,2]</p>
<div>not keyed:
<div>1</div>
<div>2</div>
</div>
<div>keyed:
<div>1</div>
<div>2</div>
</div>
`
);
shift.click();
await tick();
shift.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>add</button>
<button>shift</button>
<p>pending=2 values.length=3 values=[1,2,3]</p>
<div>not keyed:
<div>1</div>
<div>2</div>
<div>3</div>
</div>
<div>keyed:
<div>1</div>
<div>2</div>
<div>3</div>
</div>
`
);
shift.click();
await tick();
shift.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>add</button>
<button>shift</button>
<p>pending=0 values.length=4 values=[1,2,3,4]</p>
<div>not keyed:
<div>1</div>
<div>2</div>
<div>3</div>
<div>4</div>
</div>
<div>keyed:
<div>1</div>
<div>2</div>
<div>3</div>
<div>4</div>
</div>
`
);
}
});

@ -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, `<button>0</button> <button>toggle</button> 0`);
toggle?.click();
await tick();
assert.htmlEqual(target.innerHTML, `<button>0</button> <button>toggle</button>`);
toggle?.click();
await tick();
assert.htmlEqual(target.innerHTML, `<button>0</button> <button>toggle</button> 0`);
}
});

@ -0,0 +1,13 @@
<script>
import { fork } from 'svelte';
let show = $state(false);
let count = $state(0);
let d_count = $derived(count);
</script>
<button onclick={() => count += 1}>{count}</button> <!-- just here so count is compiled as a source -->
<button onclick={() => fork(() => show = !show).commit()}>toggle</button>
{#if show}
{d_count}
{/if}

@ -0,0 +1,22 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target }) {
await tick();
const [x, y, resolve, commit] = target.querySelectorAll('button');
const [p] = target.querySelectorAll('p');
y.click();
await tick();
resolve.click();
await tick();
x.click();
await tick();
assert.htmlEqual(p.innerHTML, '1 0');
await tick();
commit.click();
assert.htmlEqual(p.innerHTML, '1 1');
}
});

@ -0,0 +1,21 @@
<script>
import { fork } from 'svelte';
let x = $state(0);
let y = $state(0);
let f;
const deferred = [];
function delay(value) {
if (!value) return value;
return new Promise((resolve) => deferred.push(() => resolve(value)));
}
</script>
<p>{x} {await delay(y)}</p>
<button onclick={() => x += 1}>x</button>
<button onclick={() => f = fork(() => y += 1)}>y (fork)</button>
<button onclick={() => deferred.shift()?.()}>resolve</button>
<button onclick={() => f.commit()}>commit</button>

@ -0,0 +1,10 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
mode: ['hydrate'],
async test({ assert, target }) {
await tick();
assert.htmlEqual(target.innerHTML, `<div><div><p>first test</p></div> other test</div>`);
}
});

@ -0,0 +1,14 @@
<script>
function firstTest() {
return Promise.resolve('<p>first test</p>');
}
function otherTest() {
return Promise.resolve('other test');
}
</script>
<div>
<div>{@html await firstTest()}</div>
{await otherTest()}
</div>

@ -0,0 +1,7 @@
<script>
let data = $derived(await Promise.resolve('test'));
</script>
<div data-resolved={data ? 'true' : 'false'}>
{data}
</div>

@ -0,0 +1,7 @@
<script>
import Bound from './Bound.svelte';
let open;
</script>
<Bound bind:open />

@ -0,0 +1,10 @@
import { test } from '../../test';
// Tests that renderer.subsume (which is used when bindings are present) works correctly
export default test({
mode: ['hydrate'],
html: '<div data-resolved="true">test</div>',
async test({ assert, warnings }) {
assert.deepEqual(warnings, []);
}
});

@ -0,0 +1,7 @@
<script lang="ts">
import Async from './Async.svelte';
import Binding from './Binding.svelte';
</script>
<Async />
<Binding />

@ -6,11 +6,13 @@ export default test({
ssrHtml: '<p>yep</p>',
async test({ assert, target, variant }) {
async test({ assert, target, variant, warnings }) {
if (variant === 'dom') {
await tick();
}
assert.htmlEqual(target.innerHTML, '<p>yep</p>');
assert.deepEqual(warnings, []); // TODO not quite sure why this isn't populated yet
}
});

@ -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,
`
<button>load</button>
<button>resolve</button>
`
);
resolve.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
search search search search
<button>load</button>
<button>resolve</button>
`
);
}
});

@ -0,0 +1,40 @@
<script>
let query = $state('');
// changing the query results in a new promise with loading initialized to true
const promise = $derived(push(query));
const resolvers = [];
function push(value) {
if (!value) return Promise.resolve(value);
const { promise, resolve } = Promise.withResolvers();
resolvers.push(() => {
// before resolving, set loading to false - this makes it run in a different batch
loading = false;
resolve(value);
});
let loading = $state(true);
Object.defineProperty(promise, 'loading', {
get() {
return loading;
}
});
return promise
}
</script>
{query} {await promise}
{#if !promise.loading}
{query}
{/if}
{#if !promise.loading}
{await query}
{/if}
<button onclick={() => query = 'search'}>load</button>
<button onclick={() => resolvers.shift()?.()}>resolve</button>

@ -0,0 +1,29 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target }) {
await tick();
const [a, b] = target.querySelectorAll('button');
assert.htmlEqual(target.innerHTML, `<button>a 0</button><button>b 0</button><p>hello</p>`);
a.click();
await tick();
assert.htmlEqual(target.innerHTML, `<button>a 0</button><button>b 0</button><p>hello</p>`);
a.click();
await tick();
assert.htmlEqual(target.innerHTML, `<button>a 2</button><button>b 0</button><p>hello</p>`);
a.click();
await tick();
assert.htmlEqual(target.innerHTML, `<button>a 2</button><button>b 0</button><p>hello</p>`);
// if we don't skip over the never-resolving promise in the `else` block, we will never update
b.click();
await tick();
assert.htmlEqual(target.innerHTML, `<button>a 3</button><button>b 1</button><p>hello</p>`);
}
});

@ -0,0 +1,14 @@
<script>
let a = $state(0);
let b = $state(0);
let show = $state(true);
</script>
<button onclick={() => (a++, show = !show)}>a {a}</button>
<button onclick={() => (b++, show = !show)}>b {b}</button>
{#if show}
<p>hello</p>
{:else}
{await new Promise(() => {})}
{/if}

@ -0,0 +1,30 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target }) {
await tick();
const [a_b, b, resolve] = target.querySelectorAll('button');
a_b.click();
await tick();
assert.htmlEqual(
target.innerHTML,
'<button>a_b 0_0</button> <button>b 0</button> <button>resolve</button> 0'
);
b.click();
await tick();
assert.htmlEqual(
target.innerHTML,
'<button>a_b 0_0</button> <button>b 0</button> <button>resolve</button> 0'
);
resolve.click();
await tick();
assert.htmlEqual(
target.innerHTML,
'<button>a_b 1_2</button> <button>b 2</button> <button>resolve</button> 1'
);
}
});

@ -0,0 +1,17 @@
<script>
let a = $state(0);
let b = $state(0);
let deferreds = [];
function push(value) {
if (!value) return value;
return new Promise(resolve => {
deferreds.push(() => resolve(value));
});
}
</script>
<button onclick={() => {a++;b++}}>a_b {a}_{b}</button>
<button onclick={() => (b++)}>b {b}</button>
<button onclick={() => (deferreds.shift()?.())}>resolve</button>
{await push(a)}

@ -0,0 +1,107 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target }) {
await tick();
const [a_b, a_c, b_d, shift, pop] = target.querySelectorAll('button');
a_b.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
a 0 | b 0 | c 0 | d 0
<button>a and b</button>
<button>a and c</button>
<button>b and d</button>
<button>shift</button>
<button>pop</button>
`
);
a_c.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
a 0 | b 0 | c 0 | d 0
<button>a and b</button>
<button>a and c</button>
<button>b and d</button>
<button>shift</button>
<button>pop</button>
`
);
b_d.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
a 0 | b 0 | c 0 | d 0
<button>a and b</button>
<button>a and c</button>
<button>b and d</button>
<button>shift</button>
<button>pop</button>
`
);
shift.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
a 0 | b 0 | c 0 | d 0
<button>a and b</button>
<button>a and c</button>
<button>b and d</button>
<button>shift</button>
<button>pop</button>
`
);
shift.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
a 1 | b 1 | c 0 | d 0
<button>a and b</button>
<button>a and c</button>
<button>b and d</button>
<button>shift</button>
<button>pop</button>
`
);
shift.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
a 2 | b 1 | c 1 | d 0
<button>a and b</button>
<button>a and c</button>
<button>b and d</button>
<button>shift</button>
<button>pop</button>
`
);
shift.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
a 2 | b 2 | c 1 | d 1
<button>a and b</button>
<button>a and c</button>
<button>b and d</button>
<button>shift</button>
<button>pop</button>
`
);
}
});

@ -0,0 +1,26 @@
<script>
let a = $state(0);
let b = $state(0);
let c = $state(0);
let d = $state(0);
const deferred = [];
function delay(value) {
if (!value) return value;
return new Promise((resolve) => deferred.push(() => resolve(value)));
}
</script>
a {await delay(a)} | b {await delay(b)} | c {c} | d {d}
<button onclick={() => {a++;b++;}}>
a and b
</button>
<button onclick={() => {a++;c++;}}>
a and c
</button>
<button onclick={() => {b++;d++;}}>
b and d
</button>
<button onclick={() => deferred.shift()?.()}>shift</button>
<button onclick={() => deferred.pop()?.()}>pop</button>

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save