Merge branch 'main' into that-richan/union-component-props

pull/17348/head
Simon H 1 week ago committed by GitHub
commit 0b2fdaf492
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: unlink errored and otherwise finished batch

@ -0,0 +1,5 @@
---
'svelte': patch
---
perf: walk composedPath() directly in delegated event propagation

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: transfer effects when merging batches

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: remove temporary raw-text hydration markers

@ -1,5 +0,0 @@
---
'svelte': patch
---
fix: don't swallow `DOMException` when `media.play()` fails in `bind:paused`

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: declare `let:` directives before `{@const}` declarations on slotted elements

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: correctly coordinate component-level effects inside async blocks

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: make unnecessary commit work less likely

@ -0,0 +1,5 @@
---
"svelte": patch
---
chore: add tag name to `a11y_click_events_have_key_events` warning

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: catch rejected promises while merging/committing

@ -1,5 +0,0 @@
---
'svelte': patch
---
fix: reduce if block nesting

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

@ -0,0 +1,69 @@
name: Autofix Lint
on:
issue_comment:
types: [created]
workflow_dispatch:
permissions: {}
jobs:
autofix-lint:
permissions:
contents: write # to push the generated types commit
pull-requests: read # to resolve the PR head ref
# prevents this action from running on forks
if: |
github.repository == 'sveltejs/svelte' &&
(
github.event_name == 'workflow_dispatch' ||
(
github.event.issue.pull_request != null &&
github.event.comment.body == '/autofix' &&
contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.comment.author_association)
)
)
runs-on: ubuntu-latest
steps:
- name: Get PR ref
if: github.event_name != 'workflow_dispatch'
id: pr
uses: actions/github-script@v8
with:
script: |
const { data: pull } = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.issue.number
});
if (pull.head.repo.full_name !== `${context.repo.owner}/${context.repo.repo}`) {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: 'Cannot autofix: this PR is from a forked repository. The autofix workflow can only push to branches within this repository.'
});
core.setFailed('PR is from a fork');
}
core.setOutput('ref', pull.head.ref);
- uses: actions/checkout@v6
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@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4.3.0
- uses: actions/setup-node@v6
with:
node-version: 24
cache: pnpm
- run: pnpm install --frozen-lockfile
- name: Build
run: pnpm -F svelte build
- name: Run prettier
run: pnpm format
- name: Commit changes
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add -A
git diff --staged --quiet || git commit -m "chore: autofix"
git push origin HEAD

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

@ -4,49 +4,38 @@ on:
types: [opened, synchronize]
push:
branches: [main]
workflow_dispatch:
inputs:
sha:
description: 'Commit SHA to build'
required: true
type: string
pr:
description: 'PR number to comment on'
required: true
type: number
permissions: {}
jobs:
# This job determines the environment to use for the build job. It ensures that:
# - For pushes to main, we use the "Publish pkg.pr.new (maintainers)" environment.
# - For PRs from the same repository, we also use the "Publish pkg.pr.new (maintainers)" environment, since these are trusted.
# - For PRs from forks, we use the "Publish pkg.pr.new (external contributors)" environment, which requires manual approval by a maintainer before the build job can run.
# This protects us from running untrusted code while still allowing external contributors to use pkg.pr.new.
resolve-env:
runs-on: ubuntu-latest
outputs:
environment: ${{ steps.resolve.outputs.environment }}
steps:
- name: Determine environment
id: resolve
run: |
if [[ "${{ github.event_name }}" == "push" ]]; then
echo "environment=Publish pkg.pr.new (maintainers)" >> "$GITHUB_OUTPUT"
elif [[ "${{ github.event.pull_request.head.repo.full_name }}" == "${{ github.repository }}" ]]; then
echo "environment=Publish pkg.pr.new (maintainers)" >> "$GITHUB_OUTPUT"
else
echo "environment=Publish pkg.pr.new (external contributors)" >> "$GITHUB_OUTPUT"
fi
build:
needs: resolve-env
# Skip pull_request_target events from forks — maintainers can use workflow_dispatch instead
if: >
github.event_name != 'pull_request_target' ||
github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-latest
# This is the line that ensures forks require manual approval before running the build job
environment: ${{ needs.resolve-env.outputs.environment }}
# No permissions — this job runs user-controlled code
permissions: {}
steps:
- uses: actions/checkout@v6
with:
# For pull_request_target, we must explicitly check out the PR head.
# This is safe because the environment gate above has already fired —
# an org member has approved this specific commit for external PRs.
ref: ${{ github.event.pull_request.head.sha || github.sha }}
# For pull_request_target, check out the PR head.
# For workflow_dispatch, check out the manually specified SHA.
# 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
@ -119,10 +108,11 @@ jobs:
comment:
needs: sanitize
if: github.event_name == 'pull_request_target'
if: github.event_name == 'pull_request_target' || github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
steps:
@ -131,6 +121,27 @@ jobs:
with:
name: sanitized-output
- name: Resolve PR number
id: pr
uses: actions/github-script@v8
with:
script: |
if (context.eventName === 'pull_request_target') {
core.setOutput('number', context.issue.number);
return;
}
// For workflow_dispatch, use the explicitly provided PR number.
// We can't use listPullRequestsAssociatedWithCommit because fork
// commits don't exist in the base repo, so the API returns nothing.
const pr = Number('${{ inputs.pr }}');
if (!pr || isNaN(pr)) {
core.setFailed('workflow_dispatch requires a valid pr input');
return;
}
core.setOutput('number', pr);
- name: Post or update comment
uses: actions/github-script@v8
with:
@ -144,8 +155,7 @@ jobs:
return;
}
// Issue number from trusted event context, never from the artifact
const issue_number = context.issue.number;
const issue_number = parseInt('${{ steps.pr.outputs.number }}', 10);
const bot_comment_identifier = `<!-- pkg.pr.new comment -->`;

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

@ -31,6 +31,8 @@ packages/svelte/tests/parser-modern/samples/*/_actual.json
packages/svelte/tests/parser-modern/samples/*/output.json
packages/svelte/types
packages/svelte/compiler/index.js
playgrounds/sandbox/dist/*
playgrounds/sandbox/output/*
playgrounds/sandbox/src/*
**/node_modules

@ -0,0 +1,2 @@
https://svelte.dev/funding.json

@ -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.

@ -43,7 +43,7 @@ The maintainers meet on the final Saturday of each month. While these meetings a
### Prioritization
We do our best to review PRs and RFCs as they are sent, but it is difficult to keep up. We welcome help in reviewing PRs, RFCs, and issues. If an item aligns with the current priority on our [roadmap](https://svelte.dev/roadmap), it is more likely to be reviewed quickly. PRs to the most important and active ones repositories get reviewed more quickly while PRs to smaller inactive repos may sit for a bit before we periodically come by and review the pending PRs in a batch.
We do our best to review PRs and RFCs as they are sent, but it is difficult to keep up. We welcome help in reviewing PRs, RFCs, and issues. If an item aligns with the current priority on our [roadmap](https://svelte.dev/roadmap), it is more likely to be reviewed quickly. PRs to the most important and active repositories get reviewed more quickly while PRs to smaller inactive repos may sit for a bit before we periodically come by and review the pending PRs in a batch.
## Bugs

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

@ -167,6 +167,8 @@ To take a static snapshot of a deeply reactive `$state` proxy, use `$state.snaps
This is handy when you want to pass some state to an external library or API that doesn't expect a proxy, such as `structuredClone`.
If a value has a `toJSON` method, the snapshot will clone the value returned from `toJSON` instead of the original object.
## `$state.eager`
When state changes, it may not be reflected in the UI immediately if it is used by an `await` expression, because [updates are synchronized](await-expressions#Synchronized-updates).

@ -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(...)

@ -13,7 +13,7 @@ tags: template-each
{#each expression as name, index}...{/each}
```
Iterating over values can be done with an each block. The values in question can be arrays, array-like objects (i.e. anything with a `length` property), or iterables like `Map` and `Set`— in other words, anything that can be used with `Array.from`.
Iterating over values can be done with an each block. The values in question can be arrays, array-like objects (i.e. anything with a `length` property), or iterables like `Map` and `Set`. (Internally, they are converted to arrays with [`Array.from`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/from).)
If the value is `null` or `undefined`, it is treated the same as an empty array (which will cause [else blocks](#Else-blocks) to be rendered, where applicable).
@ -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.
@ -225,7 +241,7 @@ When the value of an `<option>` matches its text content, the attribute can be o
</select>
```
You can give the `<select>` a default value by adding a `selected` attribute to the`<option>` (or options, in the case of `<select multiple>`) that should be initially selected. If the `<select>` is part of a form, it will revert to that selection when the form is reset. Note that for the initial render the value of the binding takes precedence if it's not `undefined`.
You can give the `<select>` a default value by adding a `selected` attribute to the `<option>` (or options, in the case of `<select multiple>`) that should be initially selected. If the `<select>` is part of a form, it will revert to that selection when the form is reset. Note that for the initial render the value of the binding takes precedence if it's not `undefined`.
```svelte
<select bind:value={selected}>

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

@ -102,3 +102,40 @@ If an `onerror` function is provided, it will be called with the same two `error
```
If an error occurs inside the `onerror` function (or if you rethrow the error), it will be handled by a parent boundary if such exists.
## Using `transformError`
By default, error boundaries have no effect on the server — if an error occurs during rendering, the render as a whole will fail.
Since 5.51 you can control this behaviour for boundaries with a `failed` snippet, by calling [`render(...)`](imperative-component-api#render) with a `transformError` function.
> [!NOTE] If you're using Svelte via a framework such as SvelteKit, you most likely don't have direct access to the `render(...)` call — the framework must configure `transformError` on your behalf. SvelteKit will add support for this in the near future, via the [`handleError`](../kit/hooks#Shared-hooks-handleError) hook.
The `transformError` function must return a JSON-stringifiable object which will be used to render the `failed` snippet. This object will be serialized and used to hydrate the snippet in the browser:
```js
// @errors: 1005
import { render } from 'svelte/server';
import App from './App.svelte';
const { head, body } = await render(App, {
transformError: (error) => {
// log the original error, with the stack trace...
console.error(error);
// ...and return a sanitized user-friendly error
// to display in the `failed` snippet
return {
message: 'An error occurred!'
};
};
});
```
If `transformError` throws (or rethrows) an error, `render(...)` as a whole will fail with that error.
> [!NOTE] Errors that occur during server-side rendering can contain sensitive information in the `message` and `stack`. It's recommended to redact these rather than sending them unaltered to the browser.
If the boundary has an `onerror` handler, it will be called upon hydration with the deserialized error object.
The [`mount`](imperative-component-api#mount) and [`hydrate`](imperative-component-api#hydrate) functions also accept a `transformError` option, which defaults to the identity function. As with `render`, this function transforms a render-time error before it is passed to a `failed` snippet or `onerror` handler.

@ -10,12 +10,12 @@ title: <svelte:document>
<svelte:document bind:prop={value} />
```
Similarly to `<svelte:window>`, this element allows you to add listeners to events on `document`, such as `visibilitychange`, which don't fire on `window`. It also lets you use [actions](use) on `document`.
Similarly to `<svelte:window>`, this element allows you to add listeners to events on `document`, such as `visibilitychange`, which don't fire on `window`. It also lets you use [attachments](@attach) on `document`.
As with `<svelte:window>`, this element may only appear the top level of your component and must never be inside a block or element.
```svelte
<svelte:document onvisibilitychange={handleVisibilityChange} use:someAction />
<svelte:document onvisibilitychange={handleVisibilityChange} {@attach someAttachment} />
```
You can also bind to the following properties:

@ -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,9 @@ 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 {}
Similarly, to pass primitive values through context, use functions as described in [Passing state into functions]($state#Passing-state-into-functions).
// @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 +210,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';

@ -0,0 +1,185 @@
---
title: Best practices
skill: true
name: svelte-core-bestpractices
description: Guidance on writing fast, robust, modern Svelte code. Load this skill whenever in a Svelte project and asked to write/edit or analyze a Svelte component or module. Covers reactivity, event handling, styling, integration with libraries and more.
---
<!-- llm-ignore-start -->
This document outlines some best practices that will help you write fast, robust Svelte apps. It is also available as a `svelte-core-bestpractices` skill for your agents.
<!-- llm-ignore-end -->
## `$state`
Only use the `$state` rune for variables that should be _reactive_ — in other words, variables that cause an `$effect`, `$derived` or template expression to update. Everything else can be a normal variable.
Objects and arrays (`$state({...})` or `$state([...])`) are made deeply reactive, meaning mutation will trigger updates. This has a trade-off: in exchange for fine-grained reactivity, the objects must be proxied, which has performance overhead. In cases where you're dealing with large objects that are only ever reassigned (rather than mutated), use `$state.raw` instead. This is often the case with API responses, for example.
## `$derived`
To compute something from state, use `$derived` rather than `$effect`:
```js
// @errors: 2451
let num = 0;
// ---cut---
// do this
let square = $derived(num * num);
// don't do this
let square;
$effect(() => {
square = num * num;
});
```
> [!NOTE] `$derived` is given an expression, _not_ a function. If you need to use a function (because the expression is complex, for example) use `$derived.by`.
Deriveds are writable — you can assign to them, just like `$state`, except that they will re-evaluate when their expression changes.
If the derived expression is an object or array, it will be returned as-is — it is _not_ made deeply reactive. You can, however, use `$state` inside `$derived.by` in the rare cases that you need this.
## `$effect`
Effects are an escape hatch and should mostly be avoided. In particular, avoid updating state inside effects.
- If you need to sync state to an external library such as D3, it is often neater to use [`{@attach ...}`](@attach)
- If you need to run some code in response to user interaction, put the code directly in an event handler or use a [function binding](bind#Function-bindings) as appropriate
- If you need to log values for debugging purposes, use [`$inspect`]($inspect)
- If you need to observe something external to Svelte, use [`createSubscriber`](svelte-reactivity#createSubscriber)
Never wrap the contents of an effect in `if (browser) {...}` or similar — effects do not run on the server.
## `$props`
Treat props as though they will change. For example, values that depend on props should usually use `$derived`:
```js
// @errors: 2451
let { type } = $props();
// do this
let color = $derived(type === 'danger' ? 'red' : 'green');
// don't do this — `color` will not update if `type` changes
let color = type === 'danger' ? 'red' : 'green';
```
## `$inspect.trace`
`$inspect.trace` is a debugging tool for reactivity. If something is not updating properly or running more than it should you can add `$inspect.trace(label)` as the first line of an `$effect` or `$derived.by` (or any function they call) to trace their dependencies and discover which one triggered an update.
## Events
Any element attribute starting with `on` is treated as an event listener:
```svelte
<button onclick={() => {...}}>click me</button>
<!-- attribute shorthand also works -->
<button {onclick}>...</button>
<!-- so do spread attributes -->
<button {...props}>...</button>
```
If you need to attach listeners to `window` or `document` you can use `<svelte:window>` and `<svelte:document>`:
```svelte
<svelte:window onkeydown={...} />
<svelte:document onvisibilitychange={...} />
```
Avoid using `onMount` or `$effect` for this.
## Snippets
[Snippets](snippet) are a way to define reusable chunks of markup that can be instantiated with the [`{@render ...}`](@render) tag, or passed to components as props. They must be declared within the template.
```svelte
{#snippet greeting(name)}
<p>hello {name}!</p>
{/snippet}
{@render greeting('world')}
```
> [!NOTE] Snippets declared at the top level of a component (i.e. not inside elements or blocks) can be referenced inside `<script>`. A snippet that doesn't reference component state is also available in a `<script module>`, in which case it can be exported for use by other components.
## Each blocks
Prefer to use [keyed each blocks](each#Keyed-each-blocks) — this improves performance by allowing Svelte to surgically insert or remove items rather than updating the DOM belonging to existing items.
> [!NOTE] The key _must_ uniquely identify the object. Do not use the index as a key.
Avoid destructuring if you need to mutate the item (with something like `bind:value={item.count}`, for example).
## Using JavaScript variables in CSS
If you have a JS variable that you want to use inside CSS you can set a CSS custom property with the `style:` directive.
```svelte
<div style:--columns={columns}>...</div>
```
You can then reference `var(--columns)` inside the component's `<style>`.
## Styling child components
The CSS in a component's `<style>` is scoped to that component. If a parent component needs to control the child's styles, the preferred way is to use CSS custom properties:
```svelte
<!-- Parent.svelte -->
<Child --color="red" />
<!-- Child.svelte -->
<h1>Hello</h1>
<style>
h1 {
color: var(--color);
}
</style>
```
If this is impossible (for example, the child component comes from a library) you can use `:global` to override styles:
```svelte
<div>
<Child />
</div>
<style>
div :global {
h1 {
color: red;
}
}
</style>
```
## Context
Consider using context instead of declaring state in a shared module. This will scope the state to the part of the app that needs it, and eliminate the possibility of it leaking between users when server-side rendering.
Use `createContext` rather than `setContext` and `getContext`, as it provides type safety.
## Async Svelte
If using version 5.36 or higher, you can use [await expressions](await-expressions) and [hydratable](hydratable) to use promises directly inside components. Note that these require the `experimental.async` option to be enabled in `svelte.config.js` as they are not yet considered fully stable.
## Avoid legacy features
Always use runes mode for new code, and avoid features that have more modern replacements:
- use `$state` instead of implicit reactivity (e.g. `let count = 0; count += 1`)
- use `$derived` and `$effect` instead of `$:` assignments and statements (but only use effects when there is no better solution)
- use `$props` instead of `export let`, `$$props` and `$$restProps`
- use `onclick={...}` instead of `on:click={...}`
- use `{#snippet ...}` and `{@render ...}` instead of `<slot>` and `$$slots` and `<svelte:fragment>`
- use `<DynamicComponent>` instead of `<svelte:component this={DynamicComponent}>`
- use `import Self from './ThisComponent.svelte'` and `<Self>` instead of `<svelte:self>`
- use classes with `$state` fields to share reactivity between components, instead of using stores
- use `{@attach ...}` instead of `use:action`
- use clsx-style arrays and objects in `class` attributes, instead of the `class:` directive

@ -38,6 +38,21 @@ You can now write unit tests for code inside your `.js/.ts` files:
```js
/// file: multiplier.svelte.test.js
// @filename: multiplier.svelte.ts
export function multiplier(initial: number, k: number) {
let count = $state(initial);
return {
get value() {
return count * k;
},
set: (c: number) => {
count = c;
}
};
}
// @filename: multiplier.svelte.test.js
// ---cut---
import { flushSync } from 'svelte';
import { expect, test } from 'vitest';
import { multiplier } from './multiplier.svelte.js';
@ -80,6 +95,16 @@ Since Vitest processes your test files the same way as your source files, you ca
```js
/// file: multiplier.svelte.test.js
// @filename: multiplier.svelte.ts
export function multiplier(getCount: () => number, k: number) {
return {
get value() {
return getCount() * k;
}
};
}
// @filename: multiplier.svelte.test.js
// ---cut---
import { flushSync } from 'svelte';
import { expect, test } from 'vitest';
import { multiplier } from './multiplier.svelte.js';
@ -115,6 +140,10 @@ If the code being tested uses effects, you need to wrap the test inside `$effect
```js
/// file: logger.svelte.test.js
// @filename: logger.svelte.ts
export function logger(fn: () => void) {}
// @filename: logger.svelte.test.js
// ---cut---
import { flushSync } from 'svelte';
import { expect, test } from 'vitest';
import { logger } from './logger.svelte.js';
@ -213,7 +242,7 @@ test('Component', () => {
expect(document.body.innerHTML).toBe('<button>0</button>');
// Click the button, then flush the changes so you can synchronously write expectations
document.body.querySelector('button').click();
document.body.querySelector('button')?.click();
flushSync();
expect(document.body.innerHTML).toBe('<button>1</button>');
@ -226,6 +255,7 @@ test('Component', () => {
While the process is very straightforward, it is also low level and somewhat brittle, as the precise structure of your component may change frequently. Tools like [@testing-library/svelte](https://testing-library.com/docs/svelte-testing-library/intro/) can help streamline your tests. The above test could be rewritten like this:
```js
// @errors: 2339
/// file: component.test.js
import { render, screen } from '@testing-library/svelte';
import userEvent from '@testing-library/user-event';
@ -270,9 +300,9 @@ You can create stories for component variations and test interactions with the [
}
});
</script>
<Story name="Empty Form" />
<Story
name="Filled Form"
play={async ({ args, canvas, userEvent }) => {

@ -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 starting a new project, you can also use the [rollup](https://github.com/sveltejs/template) or [webpack](https://github.com/sveltejs/template-webpack) template to scaffold the setup from a script.
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
@ -85,7 +69,7 @@ When using TypeScript, make sure your `tsconfig.json` is setup correctly.
- Use a [`target`](https://www.typescriptlang.org/tsconfig/#target) of at least `ES2015` so classes are not compiled to functions
- Set [`verbatimModuleSyntax`](https://www.typescriptlang.org/tsconfig/#verbatimModuleSyntax) to `true` so that imports are left as-is
- Set [`isolatedModules`](https://www.typescriptlang.org/tsconfig/#isolatedModules) to `true` so that each file is looked at in isolation. TypeScript has a few features which require cross-file analysis and compilation, which the Svelte compiler and tooling like Vite don't do.
- Set [`isolatedModules`](https://www.typescriptlang.org/tsconfig/#isolatedModules) to `true` so that each file is looked at in isolation. TypeScript has a few features which require cross-file analysis and compilation, which the Svelte compiler and tooling like Vite don't do.
## Typing `$props`

@ -324,7 +324,7 @@ When spreading props, local event handlers must go _after_ the spread, or they r
>
> It was always possible to use component callback props, but because you had to listen to DOM events using `on:`, it made sense to use `createEventDispatcher` for component events due to syntactical consistency. Now that we have event attributes (`onclick`), it's the other way around: Callback props are now the more sensible thing to do.
>
> The removal of event modifiers is arguably one of the changes that seems like a step back for those who've liked the shorthand syntax of event modifiers. Given that they are not used that frequently, we traded a smaller surface area for more explicitness. Modifiers also were inconsistent, because most of them were only useable on DOM elements.
> The removal of event modifiers is arguably one of the changes that seems like a step back for those who've liked the shorthand syntax of event modifiers. Given that they are not used that frequently, we traded a smaller surface area for more explicitness. Modifiers also were inconsistent, because most of them were only usable on DOM elements.
>
> Multiple listeners for the same event are also no longer possible, but it was something of an anti-pattern anyway, since it impedes readability: if there are many attributes, it becomes harder to spot that there are two handlers unless they are right next to each other. It also implies that the two handlers are independent, when in fact something like `event.stopImmediatePropagation()` inside `one` would prevent `two` from being called.
>
@ -682,6 +682,24 @@ Previously, Svelte employed a very complicated algorithm to determine if whitesp
- Whitespace between nodes is collapsed to one whitespace
- Whitespace at the start and end of a tag is removed completely
This new behavior is slightly different from native HTML rendering. For example, `<p>foo<span> - bar</span></p>` will render:
- `foo - bar` in HTML
- `foo- bar` in Svelte 5
You can reintroduce the missing space by moving it outside the `<span>`...
```svelte
<p>foo <span>- bar</span></p>
```
...or, if necessary for styling reasons, including it as an expression:
```svelte
<p>foo<span>{' '}- bar</span></p>
```
- Certain exceptions apply such as keeping whitespace inside `pre` tags
As before, you can disable whitespace trimming by setting the `preserveWhitespace` option in your compiler settings or on a per-component basis in `<svelte:options>`.

@ -62,6 +62,14 @@ Keyed each block has duplicate key at indexes %a% and %b%
Keyed each block has duplicate key `%value%` at indexes %a% and %b%
```
### each_key_volatile
```
Keyed each block has key that is not idempotent — the key for item at index %index% was `%a%` but is now `%b%`. Keys must be the same each time for a given item
```
The key expression in a keyed each block must return the same value when called multiple times for the same item. Using expressions like `[item.a, item.b]` creates a new array each time, which will never be equal to itself. Instead, use a primitive value or create a stable key like `item.a + '-' + item.b`.
### effect_in_teardown
```

@ -134,6 +134,14 @@ When logging a [proxy](https://developer.mozilla.org/en-US/docs/Web/JavaScript/R
The easiest way to log a value as it changes over time is to use the [`$inspect`](/docs/svelte/$inspect) rune. Alternatively, to log things on a one-off basis (for example, inside an event handler) you can use [`$state.snapshot`](/docs/svelte/$state#$state.snapshot) to take a snapshot of the current value.
### derived_inert
```
Reading a derived belonging to a now-destroyed effect may result in stale values
```
A `$derived` value created inside an effect will stop updating when the effect is destroyed. You should create the `$derived` outside the effect, or inside an `$effect.root`.
### event_handler_invalid
```

@ -63,9 +63,29 @@ Event attribute must be a JavaScript expression, not a string
### attribute_invalid_sequence_expression
```
Sequence expressions are not allowed as attribute/directive values in runes mode, unless wrapped in parentheses
Comma-separated expressions are not allowed as attribute/directive values in runes mode, unless wrapped in parentheses
```
An attribute value cannot be a comma-separated sequence of expressions — in other words this is disallowed:
```svelte
<div class={size, color}>...</div>
```
Instead, make sure that the attribute value contains a single expression. In the example above it's likely that this was intended (see the [class documentation](class) for more details):
```svelte
<div class={[size, color]}>...</div>
```
If you _do_ need to use the comma operator for some reason, wrap the sequence in parentheses:
```svelte
<div class={(size, color)}>...</div>
```
Note that this will evaluate to `color`, ignoring `size`.
### attribute_invalid_type
```
@ -376,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

@ -62,7 +62,7 @@ Enforce that `autofocus` is not used on elements. Autofocusing elements can caus
### a11y_click_events_have_key_events
```
Visible, non-interactive elements with a click event must be accompanied by a keyboard event handler. Consider whether an interactive element such as `<button type="button">` or `<a>` might be more appropriate
Visible, non-interactive element `<%element%>` with a click event must be accompanied by a keyboard event handler. Consider whether an interactive element such as `<button type="button">` or `<a>` might be more appropriate
```
Enforce that visible, non-interactive elements with an `onclick` event are accompanied by a keyboard event handler.

@ -16,6 +16,14 @@ Encountered asynchronous work while rendering synchronously.
You (or the framework you're using) called [`render(...)`](svelte-server#render) with a component containing an `await` expression. Either `await` the result of `render` or wrap the `await` (or the component containing it) in a [`<svelte:boundary>`](svelte-boundary) with a `pending` snippet.
### dynamic_element_invalid_tag
```
`<svelte:element this="%tag%">` is not a valid element name — the element will not be rendered
```
The value passed to the `this` prop of `<svelte:element>` must be a valid HTML element, SVG element, MathML element, or custom element name. A value containing invalid characters (such as whitespace or special characters) was provided, which could be a security risk. Ensure only valid tag names are passed.
### html_deprecated
```
@ -61,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
```

@ -42,6 +42,12 @@ Here, `List.svelte` is using `{@render children(item)` which means it expects `P
A snippet function was passed invalid arguments. Snippets should only be instantiated via `{@render ...}`
```
### invariant_violation
```
An invariant violation occurred, meaning Svelte's internal assumptions were flawed. This is a bug in Svelte, not your app — please open an issue at https://github.com/sveltejs/svelte, citing the following message: "%message%"
```
### lifecycle_outside_component
```

@ -27,12 +27,12 @@
},
"devDependencies": {
"@changesets/cli": "^2.29.8",
"@sveltejs/eslint-config": "^8.3.5",
"@eslint/js": "^10.0.0",
"@sveltejs/eslint-config": "^9.0.0",
"@svitejs/changesets-changelog-github-compact": "^1.1.0",
"@types/node": "^20.11.5",
"@types/picomatch": "^4.0.2",
"@vitest/coverage-v8": "^2.1.9",
"@eslint/js": "^10.0.0",
"eslint": "^10.0.0",
"eslint-plugin-lube": "^0.5.1",
"eslint-plugin-svelte": "^3.15.0",
@ -42,15 +42,8 @@
"prettier-plugin-svelte": "^3.4.0",
"svelte": "workspace:^",
"typescript": "^5.5.4",
"typescript-eslint": "^8.55.0",
"typescript-eslint": "^8.56.0",
"v8-natives": "^1.2.5",
"vitest": "^2.1.9"
},
"pnpm": {
"peerDependencyRules": {
"allowedVersions": {
"eslint": "10"
}
}
}
}

@ -1,5 +1,471 @@
# svelte
## 5.55.9
### Patch Changes
- fix: don't unset batch when calling `{#await ...}` promise ([#18243](https://github.com/sveltejs/svelte/pull/18243))
- fix: promise-ify `{#await await ...}` expressions on the server and correctly hydrate them on the client ([#18243](https://github.com/sveltejs/svelte/pull/18243))
- fix: deduplicate dependencies that are added outside the init/update cycle ([#18243](https://github.com/sveltejs/svelte/pull/18243))
- fix: avoid false-positive batch invariant error ([#18246](https://github.com/sveltejs/svelte/pull/18246))
- fix: inline primitive constants in attribute values during SSR ([#18232](https://github.com/sveltejs/svelte/pull/18232))
## 5.55.8
### Patch Changes
- fix(print): handle `svelte:body` and fix keyframe percentage double-printing ([#18234](https://github.com/sveltejs/svelte/pull/18234))
- fix: execute uninitialized derived even if it's destroyed ([#18228](https://github.com/sveltejs/svelte/pull/18228))
- fix: use named symbols everywhere ([#18238](https://github.com/sveltejs/svelte/pull/18238))
- fix: don't run teardown effects when deriveds are unfreezed ([#18227](https://github.com/sveltejs/svelte/pull/18227))
- fix: unset context synchronously in `run` ([#18236](https://github.com/sveltejs/svelte/pull/18236))
## 5.55.7
### Patch Changes
- fix: prevent XSS on `hydratable` from user contents ([`a16ebc67bbcf8f708360195687e1b2719463e1a4`](https://github.com/sveltejs/svelte/commit/a16ebc67bbcf8f708360195687e1b2719463e1a4))
- chore: bump devalue ([#18219](https://github.com/sveltejs/svelte/pull/18219))
- fix: disallow empty attribute names during SSR ([`547853e2406a2147ad7fb5ffeba95b01bd9642da`](https://github.com/sveltejs/svelte/commit/547853e2406a2147ad7fb5ffeba95b01bd9642da))
- fix: harden regex ([`d2375e2ebcab5c88feb5652f1a9d621b8f06b259`](https://github.com/sveltejs/svelte/commit/d2375e2ebcab5c88feb5652f1a9d621b8f06b259))
- fix: move Svelte runtime properties to symbols ([`e1cbbd96441e82c9eb8a23a2903c0d06d3cda991`](https://github.com/sveltejs/svelte/commit/e1cbbd96441e82c9eb8a23a2903c0d06d3cda991))
## 5.55.6
### Patch Changes
- fix: leave stale promises to wait for a later resolution, instead of rejecting ([#18180](https://github.com/sveltejs/svelte/pull/18180))
- fix: keep dependencies of `$state.eager/pending` ([#18218](https://github.com/sveltejs/svelte/pull/18218))
- fix: reapply context after transforming error during SSR ([#18099](https://github.com/sveltejs/svelte/pull/18099))
- fix: don't rebase just-created batches ([#18117](https://github.com/sveltejs/svelte/pull/18117))
- chore: allow `null` for `pending` in typings ([#18201](https://github.com/sveltejs/svelte/pull/18201))
- fix: flush eager effects in production ([#18107](https://github.com/sveltejs/svelte/pull/18107))
- fix: rethrow error of failed iterable after calling `return()` ([#18169](https://github.com/sveltejs/svelte/pull/18169))
- fix: account for proxified instance when updating `bind:this` ([#18147](https://github.com/sveltejs/svelte/pull/18147))
- fix: ensure scheduled batch is flushed if not obsolete ([#18131](https://github.com/sveltejs/svelte/pull/18131))
- fix: resolve stale deriveds with latest value ([#18167](https://github.com/sveltejs/svelte/pull/18167))
- chore: remove unnecessary `increment_pending` calls ([#18183](https://github.com/sveltejs/svelte/pull/18183))
- fix: correctly compile component member expressions for SSR ([#18192](https://github.com/sveltejs/svelte/pull/18192))
- fix: reset `source.updated` stack traces after `flush` ([#18196](https://github.com/sveltejs/svelte/pull/18196))
- fix: replacing async 'blocking' strategy with 'merging' ([#18205](https://github.com/sveltejs/svelte/pull/18205))
- fix: allow `@debug` tags to reference awaited variables ([#18138](https://github.com/sveltejs/svelte/pull/18138))
- fix: re-run fallback props if dependencies update ([#18146](https://github.com/sveltejs/svelte/pull/18146))
- fix: abort running obsolete async branches ([#18118](https://github.com/sveltejs/svelte/pull/18118))
- fix: ignore comments when reading CSS values ([#18153](https://github.com/sveltejs/svelte/pull/18153))
- fix: wrap `Promise.all` in `save` during SSR ([#18178](https://github.com/sveltejs/svelte/pull/18178))
- fix: ignore false-positive errors of `$inspect` dependencies ([#18106](https://github.com/sveltejs/svelte/pull/18106))
## 5.55.5
### Patch Changes
- fix: don't mark deriveds while an effect is updating ([#18124](https://github.com/sveltejs/svelte/pull/18124))
- fix: do not dispatch introstart event with animation of animate directive ([#18122](https://github.com/sveltejs/svelte/pull/18122))
## 5.55.4
### Patch Changes
- fix: never mark a child effect root as inert ([#18111](https://github.com/sveltejs/svelte/pull/18111))
- fix: reset context after waiting on blockers of `@const` expressions ([#18100](https://github.com/sveltejs/svelte/pull/18100))
- fix: keep flushing new eager effects ([#18102](https://github.com/sveltejs/svelte/pull/18102))
## 5.55.3
### Patch Changes
- fix: ensure proper HMR updates for dynamic components ([#18079](https://github.com/sveltejs/svelte/pull/18079))
- fix: correctly calculate `@const` blockers ([#18039](https://github.com/sveltejs/svelte/pull/18039))
- fix: freeze deriveds once their containing effects are destroyed ([#17921](https://github.com/sveltejs/svelte/pull/17921))
- fix: defer error boundary rendering in forks ([#18076](https://github.com/sveltejs/svelte/pull/18076))
- fix: avoid false positives for reactivity loss warning ([#18088](https://github.com/sveltejs/svelte/pull/18088))
## 5.55.2
### Patch Changes
- fix: invalidate `@const` tags based on visible references in legacy mode ([#18041](https://github.com/sveltejs/svelte/pull/18041))
- fix: handle parens in template expressions more robustly ([#18075](https://github.com/sveltejs/svelte/pull/18075))
- fix: disallow `--` in `idPrefix` ([#18038](https://github.com/sveltejs/svelte/pull/18038))
- fix: correct types for `ontoggle` on `<details>` elements ([#18063](https://github.com/sveltejs/svelte/pull/18063))
- fix: don't override `$destroy/set/on` instance methods in dev mode ([#18034](https://github.com/sveltejs/svelte/pull/18034))
- fix: unskip branches of earlier batches after commit ([#18048](https://github.com/sveltejs/svelte/pull/18048))
- fix: never set derived.v inside fork ([#18037](https://github.com/sveltejs/svelte/pull/18037))
- fix: skip rebase logic in non-async mode ([#18040](https://github.com/sveltejs/svelte/pull/18040))
- fix: don't reset status of uninitialized deriveds ([#18054](https://github.com/sveltejs/svelte/pull/18054))
## 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
- feat: allow `css`, `runes`, `customElement` compiler options to be functions ([#17951](https://github.com/sveltejs/svelte/pull/17951))
### Patch Changes
- fix: reinstate reactivity loss tracking ([#17801](https://github.com/sveltejs/svelte/pull/17801))
## 5.53.13
### Patch Changes
- fix: ensure `$inspect` after top level await doesn't break builds ([#17943](https://github.com/sveltejs/svelte/pull/17943))
- fix: resume inert effects when they come from offscreen ([#17942](https://github.com/sveltejs/svelte/pull/17942))
- fix: don't eagerly access not-yet-initialized functions in template ([#17938](https://github.com/sveltejs/svelte/pull/17938))
- fix: discard batches made obsolete by commit ([#17934](https://github.com/sveltejs/svelte/pull/17934))
- fix: ensure "is standalone child" is correctly reset ([#17944](https://github.com/sveltejs/svelte/pull/17944))
- fix: remove nodes in boundary when work is pending and HMR is active ([#17932](https://github.com/sveltejs/svelte/pull/17932))
## 5.53.12
### Patch Changes
- fix: update `select.__value` on `change` ([#17745](https://github.com/sveltejs/svelte/pull/17745))
- chore: add `invariant` helper for debugging ([#17929](https://github.com/sveltejs/svelte/pull/17929))
- fix: ensure deriveds values are correct across batches ([#17917](https://github.com/sveltejs/svelte/pull/17917))
- fix: handle async RHS in `assignment_value_stale` ([#17925](https://github.com/sveltejs/svelte/pull/17925))
- fix: avoid traversing clean roots ([#17928](https://github.com/sveltejs/svelte/pull/17928))
## 5.53.11
### Patch Changes
- fix: remove `untrack` circular dependency ([#17910](https://github.com/sveltejs/svelte/pull/17910))
- fix: recover from errors that leave a corrupted effect tree ([#17888](https://github.com/sveltejs/svelte/pull/17888))
- fix: properly lazily evaluate RHS when checking for `assignment_value_stale` ([#17906](https://github.com/sveltejs/svelte/pull/17906))
- fix: resolve boundary in correct batch when hydrating ([#17914](https://github.com/sveltejs/svelte/pull/17914))
- chore: rebase batches after process, not during ([#17900](https://github.com/sveltejs/svelte/pull/17900))
## 5.53.10
### Patch Changes
- fix: re-process batch if new root effects were scheduled ([#17895](https://github.com/sveltejs/svelte/pull/17895))
## 5.53.9
### Patch Changes
- fix: better `bind:this` cleanup timing ([#17885](https://github.com/sveltejs/svelte/pull/17885))
## 5.53.8
### Patch Changes
- fix: `{@html}` no longer duplicates content inside `contenteditable` elements ([#17853](https://github.com/sveltejs/svelte/pull/17853))
- fix: don't access inert block effects ([#17882](https://github.com/sveltejs/svelte/pull/17882))
- fix: handle asnyc updates within pending boundary ([#17873](https://github.com/sveltejs/svelte/pull/17873))
- perf: avoid re-traversing the effect tree after `$:` assignments ([#17848](https://github.com/sveltejs/svelte/pull/17848))
- chore: simplify scheduling logic ([#17805](https://github.com/sveltejs/svelte/pull/17805))
## 5.53.7
### Patch Changes
- fix: correctly add \_\_svelte_meta after else-if chains ([#17830](https://github.com/sveltejs/svelte/pull/17830))
- perf: cache element interactivity and source line splitting in compiler ([#17839](https://github.com/sveltejs/svelte/pull/17839))
- chore: avoid rescheduling effects during branch commit ([#17837](https://github.com/sveltejs/svelte/pull/17837))
- perf: optimize CSS selector pruning ([#17846](https://github.com/sveltejs/svelte/pull/17846))
- fix: preserve original boundary errors when keyed each rows are removed during async updates ([#17843](https://github.com/sveltejs/svelte/pull/17843))
- perf: avoid O(n²) name scanning in scope `generate` and `unique` ([#17844](https://github.com/sveltejs/svelte/pull/17844))
- fix: preserve each items that are needed by pending batches ([#17819](https://github.com/sveltejs/svelte/pull/17819))
## 5.53.6
### Patch Changes
- perf: optimize parser hot paths for faster compilation ([#17811](https://github.com/sveltejs/svelte/pull/17811))
- fix: `SvelteMap` incorrectly handles keys with `undefined` values ([#17826](https://github.com/sveltejs/svelte/pull/17826))
- fix: SvelteURL `search` setter now returns the normalized value, matching native URL behavior ([#17828](https://github.com/sveltejs/svelte/pull/17828))
- fix: visit synthetic value node during ssr ([#17824](https://github.com/sveltejs/svelte/pull/17824))
- fix: always case insensitive event handlers during ssr ([#17822](https://github.com/sveltejs/svelte/pull/17822))
- chore: more efficient effect scheduling ([#17808](https://github.com/sveltejs/svelte/pull/17808))
- perf: optimize compiler analysis phase ([#17823](https://github.com/sveltejs/svelte/pull/17823))
- fix: skip redundant batch.apply ([#17816](https://github.com/sveltejs/svelte/pull/17816))
- chore: null out current_batch before committing branches ([#17809](https://github.com/sveltejs/svelte/pull/17809))
## 5.53.5
### Patch Changes
- fix: escape `innerText` and `textContent` bindings of `contenteditable` ([`0df5abcae223058ceb95491470372065fb87951d`](https://github.com/sveltejs/svelte/commit/0df5abcae223058ceb95491470372065fb87951d))
- fix: sanitize `transformError` values prior to embedding in HTML comments ([`0298e979371bb583855c9810db79a70a551d22b9`](https://github.com/sveltejs/svelte/commit/0298e979371bb583855c9810db79a70a551d22b9))
## 5.53.4
### Patch Changes
- fix: set server context after async transformError ([#17799](https://github.com/sveltejs/svelte/pull/17799))
- fix: hydrate if blocks correctly ([#17784](https://github.com/sveltejs/svelte/pull/17784))
- fix: handle default parameters scope leaks ([#17788](https://github.com/sveltejs/svelte/pull/17788))
- fix: prevent flushed effects from running again ([#17787](https://github.com/sveltejs/svelte/pull/17787))
## 5.53.3
### Patch Changes
- fix: render `:catch` of `#await` block with correct key ([#17769](https://github.com/sveltejs/svelte/pull/17769))
- chore: pin aria-query@5.3.1 ([#17772](https://github.com/sveltejs/svelte/pull/17772))
- fix: make string coercion consistent to `toString` ([#17774](https://github.com/sveltejs/svelte/pull/17774))
## 5.53.2
### Patch Changes
- fix: update expressions on server deriveds ([#17767](https://github.com/sveltejs/svelte/pull/17767))
- fix: further obfuscate `node:crypto` import from overzealous static analysis ([#17763](https://github.com/sveltejs/svelte/pull/17763))
## 5.53.1
### Patch Changes
- fix: handle shadowed function names correctly ([#17753](https://github.com/sveltejs/svelte/pull/17753))
## 5.53.0
### Minor Changes
- feat: allow comments in tags ([#17671](https://github.com/sveltejs/svelte/pull/17671))
- feat: allow error boundaries to work on the server ([#17672](https://github.com/sveltejs/svelte/pull/17672))
### Patch Changes
- fix: use TrustedHTML to test for customizable `<select>` support, where necessary ([#17743](https://github.com/sveltejs/svelte/pull/17743))
- fix: ensure head effects are kept in the effect tree ([#17746](https://github.com/sveltejs/svelte/pull/17746))
- chore: deactivate current_batch by default in unset_context ([#17738](https://github.com/sveltejs/svelte/pull/17738))
## 5.52.0
### Minor Changes
- feat: support TrustedHTML in `{@html}` expressions ([#17701](https://github.com/sveltejs/svelte/pull/17701))
### Patch Changes
- fix: repair dynamic component truthy/falsy hydration mismatches ([#17737](https://github.com/sveltejs/svelte/pull/17737))
- fix: re-run non-render-bound deriveds on the server ([#17674](https://github.com/sveltejs/svelte/pull/17674))
## 5.51.5
### Patch Changes
- fix: check to make sure `svelte:element` tags are valid during SSR ([`73098bb26c6f06e7fd1b0746d817d2c5ee90755f`](https://github.com/sveltejs/svelte/commit/73098bb26c6f06e7fd1b0746d817d2c5ee90755f))
- fix: misc option escaping and backwards compatibility ([#17741](https://github.com/sveltejs/svelte/pull/17741))
- fix: strip event handlers during SSR ([`a0c7f289156e9fafaeaf5ca14af6c06fe9b9eae5`](https://github.com/sveltejs/svelte/commit/a0c7f289156e9fafaeaf5ca14af6c06fe9b9eae5))
- fix: replace usage of `for in` with `for of Object.keys` ([`f89c7ddd7eebaa1ef3cc540400bec2c9140b330c`](https://github.com/sveltejs/svelte/commit/f89c7ddd7eebaa1ef3cc540400bec2c9140b330c))
- fix: always escape option body in SSR ([`f7c80da18c215e3727c2a611b0b8744cc6e504c5`](https://github.com/sveltejs/svelte/commit/f7c80da18c215e3727c2a611b0b8744cc6e504c5))
- chore: upgrade `devalue` ([#17739](https://github.com/sveltejs/svelte/pull/17739))
## 5.51.4
### Patch Changes
- chore: proactively defer effects in pending boundary ([#17734](https://github.com/sveltejs/svelte/pull/17734))
- fix: detect and error on non-idempotent each block keys in dev mode ([#17732](https://github.com/sveltejs/svelte/pull/17732))
## 5.51.3
### Patch Changes
- fix: prevent event delegation logic conflicting between svelte instances ([#17728](https://github.com/sveltejs/svelte/pull/17728))
- fix: treat CSS attribute selectors as case-insensitive for HTML enumerated attributes ([#17712](https://github.com/sveltejs/svelte/pull/17712))
- fix: locate Rollup annontaion friendly to JS downgraders ([#17724](https://github.com/sveltejs/svelte/pull/17724))
- fix: run effects in pending snippets ([#17719](https://github.com/sveltejs/svelte/pull/17719))
## 5.51.2
### Patch Changes
- fix: take async into consideration for dev delegated handlers ([#17710](https://github.com/sveltejs/svelte/pull/17710))
- fix: emit state_referenced_locally warning for non-destructured props ([#17708](https://github.com/sveltejs/svelte/pull/17708))
## 5.51.1
### Patch Changes
- fix: don't crash on undefined `document.contentType` ([#17707](https://github.com/sveltejs/svelte/pull/17707))
- fix: use symbols for encapsulated event delegation ([#17703](https://github.com/sveltejs/svelte/pull/17703))
## 5.51.0
### Minor Changes
- feat: Use `TrustedTypes` for HTML handling where supported ([#16271](https://github.com/sveltejs/svelte/pull/16271))
### Patch Changes
- fix: sanitize template-literal-special-characters in SSR attribute values ([#17692](https://github.com/sveltejs/svelte/pull/17692))
- fix: follow-up formatting in `print()` — flush block-level elements into separate sequences ([#17699](https://github.com/sveltejs/svelte/pull/17699))
- fix: preserve delegated event handlers as long as one or more root components are using them ([#17695](https://github.com/sveltejs/svelte/pull/17695))
## 5.50.3
### Patch Changes
- fix: take into account `nodeName` case sensitivity on XHTML pages ([#17689](https://github.com/sveltejs/svelte/pull/17689))
- fix: render `multiple` and `selected` attributes as empty strings for XHTML compliance ([#17689](https://github.com/sveltejs/svelte/pull/17689))
- fix: always lowercase HTML elements, for XHTML compliance ([#17664](https://github.com/sveltejs/svelte/pull/17664))
- fix: freeze effects-inside-deriveds when disconnecting, unfreeze on reconnect ([#17682](https://github.com/sveltejs/svelte/pull/17682))
- fix: propagate `$effect` errors to `<svelte:boundary>` ([#17684](https://github.com/sveltejs/svelte/pull/17684))
## 5.50.2
### Patch Changes
- fix: resolve `effect_update_depth_exceeded` when using `bind:value` on `<select>` with derived state in legacy mode ([#17645](https://github.com/sveltejs/svelte/pull/17645))
- fix: don't swallow `DOMException` when `media.play()` fails in `bind:paused` ([#17656](https://github.com/sveltejs/svelte/pull/17656))
- chore: provide proper public type for `parseCss` result ([#17654](https://github.com/sveltejs/svelte/pull/17654))
- fix: robustify blocker calculation ([#17676](https://github.com/sveltejs/svelte/pull/17676))
- fix: reduce if block nesting ([#17662](https://github.com/sveltejs/svelte/pull/17662))
## 5.50.1
### Patch 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> {
@ -2067,9 +2067,9 @@ export interface SvelteHTMLElements {
};
'svelte:head': { [name: string]: any };
'svelte:boundary': {
onerror?: (error: unknown, reset: () => void) => void;
failed?: import('svelte').Snippet<[error: unknown, reset: () => void]>;
pending?: import('svelte').Snippet;
onerror?: ((error: unknown, reset: () => void) => void) | null | undefined;
failed?: import('svelte').Snippet<[error: unknown, reset: () => void]> | null | undefined;
pending?: import('svelte').Snippet | null | undefined;
};
[name: string]: { [name: string]: any };

@ -42,6 +42,12 @@ See the [migration guide](/docs/svelte/v5-migration-guide#Components-are-no-long
> Keyed each block has duplicate key `%value%` at indexes %a% and %b%
## each_key_volatile
> Keyed each block has key that is not idempotent — the key for item at index %index% was `%a%` but is now `%b%`. Keys must be the same each time for a given item
The key expression in a keyed each block must return the same value when called multiple times for the same item. Using expressions like `[item.a, item.b]` creates a new array each time, which will never be equal to itself. Instead, use a primitive value or create a stable key like `item.a + '-' + item.b`.
## effect_in_teardown
> `%rune%` cannot be used inside an effect cleanup function

@ -120,6 +120,12 @@ When logging a [proxy](https://developer.mozilla.org/en-US/docs/Web/JavaScript/R
The easiest way to log a value as it changes over time is to use the [`$inspect`](/docs/svelte/$inspect) rune. Alternatively, to log things on a one-off basis (for example, inside an event handler) you can use [`$state.snapshot`](/docs/svelte/$state#$state.snapshot) to take a snapshot of the current value.
## derived_inert
> Reading a derived belonging to a now-destroyed effect may result in stale values
A `$derived` value created inside an effect will stop updating when the effect is destroyed. You should create the `$derived` outside the effect, or inside an `$effect.root`.
## event_handler_invalid
> %handler% should be a function. Did you mean to %suggestion%?

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

@ -40,7 +40,27 @@
## attribute_invalid_sequence_expression
> Sequence expressions are not allowed as attribute/directive values in runes mode, unless wrapped in parentheses
> Comma-separated expressions are not allowed as attribute/directive values in runes mode, unless wrapped in parentheses
An attribute value cannot be a comma-separated sequence of expressions — in other words this is disallowed:
```svelte
<div class={size, color}>...</div>
```
Instead, make sure that the attribute value contains a single expression. In the example above it's likely that this was intended (see the [class documentation](class) for more details):
```svelte
<div class={[size, color]}>...</div>
```
If you _do_ need to use the comma operator for some reason, wrap the sequence in parentheses:
```svelte
<div class={(size, color)}>...</div>
```
Note that this will evaluate to `color`, ignoring `size`.
## attribute_invalid_type

@ -49,7 +49,7 @@ Enforce that `autofocus` is not used on elements. Autofocusing elements can caus
## a11y_click_events_have_key_events
> Visible, non-interactive elements with a click event must be accompanied by a keyboard event handler. Consider whether an interactive element such as `<button type="button">` or `<a>` might be more appropriate
> Visible, non-interactive element `<%element%>` with a click event must be accompanied by a keyboard event handler. Consider whether an interactive element such as `<button type="button">` or `<a>` might be more appropriate
Enforce that visible, non-interactive elements with an `onclick` event are accompanied by a keyboard event handler.

@ -10,6 +10,12 @@ Some platforms require configuration flags to enable this API. Consult your plat
You (or the framework you're using) called [`render(...)`](svelte-server#render) with a component containing an `await` expression. Either `await` the result of `render` or wrap the `await` (or the component containing it) in a [`<svelte:boundary>`](svelte-boundary) with a `pending` snippet.
## dynamic_element_invalid_tag
> `<svelte:element this="%tag%">` is not a valid element name — the element will not be rendered
The value passed to the `this` prop of `<svelte:element>` must be a valid HTML element, SVG element, MathML element, or custom element name. A value containing invalid characters (such as whitespace or special characters) was provided, which could be a security risk. Ensure only valid tag names are passed.
## html_deprecated
> The `html` property of server render results has been deprecated. Use `body` instead.
@ -47,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

@ -34,6 +34,10 @@ Here, `List.svelte` is using `{@render children(item)` which means it expects `P
> A snippet function was passed invalid arguments. Snippets should only be instantiated via `{@render ...}`
## invariant_violation
> An invariant violation occurred, meaning Svelte's internal assumptions were flawed. This is a bug in Svelte, not your app — please open an issue at https://github.com/sveltejs/svelte, citing the following message: "%message%"
## lifecycle_outside_component
> `%name%(...)` can only be used during component initialisation

@ -2,7 +2,7 @@
"name": "svelte",
"description": "Cybernetically enhanced web apps",
"license": "MIT",
"version": "5.50.1",
"version": "5.55.9",
"type": "module",
"types": "./types/index.d.ts",
"engines": {
@ -138,14 +138,15 @@
"templating"
],
"scripts": {
"build": "node scripts/process-messages && rollup -c && pnpm generate:types && node scripts/check-treeshakeability.js",
"build": "rollup -c && pnpm generate",
"dev": "node scripts/process-messages -w & rollup -cw",
"check": "tsc --project tsconfig.runtime.json && tsc && cd ./tests/types && tsc",
"check:tsgo": "tsgo --project tsconfig.runtime.json --skipLibCheck && tsgo --skipLibCheck",
"check:watch": "tsc --watch",
"generate": "node scripts/process-messages && node ./scripts/generate-types.js",
"generate:version": "node ./scripts/generate-version.js",
"generate:types": "node ./scripts/generate-types.js && tsc -p tsconfig.generated.json",
"prepublishOnly": "pnpm build",
"prepublishOnly": "pnpm build && node scripts/check-treeshakeability.js",
"knip": "pnpm dlx knip"
},
"devDependencies": {
@ -159,7 +160,7 @@
"@types/node": "^20.11.5",
"dts-buddy": "^0.5.5",
"esbuild": "^0.25.10",
"rollup": "^4.22.4",
"rollup": "^4.59.0",
"source-map": "^0.7.4",
"tinyglobby": "^0.2.12",
"typescript": "^5.5.4",
@ -168,15 +169,16 @@
"dependencies": {
"@jridgewell/remapping": "^2.3.4",
"@jridgewell/sourcemap-codec": "^1.5.0",
"@sveltejs/acorn-typescript": "^1.0.5",
"@sveltejs/acorn-typescript": "^1.0.10",
"@types/estree": "^1.0.5",
"@types/trusted-types": "^2.0.7",
"acorn": "^8.12.1",
"aria-query": "^5.3.1",
"aria-query": "5.3.1",
"axobject-query": "^4.1.0",
"clsx": "^2.1.1",
"devalue": "^5.6.2",
"devalue": "^5.8.1",
"esm-env": "^1.2.1",
"esrap": "^2.2.2",
"esrap": "^2.2.9",
"is-reference": "^3.0.3",
"locate-character": "^3.0.0",
"magic-string": "^0.30.11",

@ -378,7 +378,6 @@ function run() {
};
const block = esrap.print(
// @ts-expect-error some bullshit
/** @type {ESTree.Program} */ ({ ...ast, body: [clone] }),
ts({ comments: [jsdoc_clone] })
).code;

@ -147,6 +147,8 @@ declare namespace $state {
* </script>
* ```
*
* If `state` has a `toJSON` method, the snapshot will clone the value returned from `toJSON` instead of the original object.
*
* @see {@link https://svelte.dev/docs/svelte/$state#$state.snapshot Documentation}
*
* @param state The value to snapshot

@ -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`);
}
/**
@ -785,12 +785,12 @@ export function attribute_invalid_name(node, name) {
}
/**
* Sequence expressions are not allowed as attribute/directive values in runes mode, unless wrapped in parentheses
* Comma-separated expressions are not allowed as attribute/directive values in runes mode, unless wrapped in parentheses
* @param {null | number | NodeLike} node
* @returns {never}
*/
export function attribute_invalid_sequence_expression(node) {
e(node, 'attribute_invalid_sequence_expression', `Sequence expressions are not allowed as attribute/directive values in runes mode, unless wrapped in parentheses\nhttps://svelte.dev/e/attribute_invalid_sequence_expression`);
e(node, 'attribute_invalid_sequence_expression', `Comma-separated expressions are not allowed as attribute/directive values in runes mode, unless wrapped in parentheses\nhttps://svelte.dev/e/attribute_invalid_sequence_expression`);
}
/**

@ -23,6 +23,7 @@ export { print } from './print/index.js';
export function compile(source, options) {
source = remove_bom(source);
state.reset({ warning: options.warningFilter, filename: options.filename });
const validated = validate_component_options(options, '');
let parsed = _parse(source);
@ -33,7 +34,9 @@ export function compile(source, options) {
const combined_options = {
...validated,
...parsed_options,
customElementOptions
customElementOptions,
css: 'css' in parsed_options ? () => parsed_options.css ?? 'external' : validated.css,
runes: 'runes' in parsed_options ? () => parsed_options.runes : validated.runes
};
if (parsed.metadata.ts) {
@ -123,7 +126,7 @@ export function parse(source, { modern, loose } = {}) {
* The parseCss function parses a CSS stylesheet, returning its abstract syntax tree.
*
* @param {string} source The CSS source code
* @returns {Omit<AST.CSS.StyleSheet, 'attributes' | 'content'>}
* @returns {AST.CSS.StyleSheetFile}
*/
export function parseCss(source) {
source = remove_bom(source);
@ -135,7 +138,7 @@ export function parseCss(source) {
const children = parse_stylesheet(parser);
return {
type: 'StyleSheet',
type: 'StyleSheetFile',
start: 0,
end: source.length,
children

@ -101,7 +101,12 @@ export function convert(source, ast) {
},
instance,
module,
css: ast.css ? visit(ast.css) : undefined
css: ast.css ? visit(ast.css) : undefined,
// put it on _comments not comments because the latter is checked by prettier and then fails
// if we don't adjust stuff accordingly in our prettier plugin, and so it would be kind of an
// indirect breaking change for people updating their Svelte version but not their prettier plugin version.
// We can keep it as comments for the modern AST because the modern AST is not used in the plugin yet.
_comments: ast.comments?.length > 0 ? ast.comments : undefined
};
},
AnimateDirective(node) {

@ -146,6 +146,8 @@ export function migrate(source, { filename, use_ts } = {}) {
...parsed_options,
customElementOptions,
filename: filename ?? UNKNOWN_FILENAME,
css: 'css' in parsed_options ? () => parsed_options.css ?? 'external' : () => 'external',
runes: 'runes' in parsed_options ? () => parsed_options.runes : () => undefined,
experimental: {
async: true
}

@ -1,10 +1,13 @@
/** @import { Comment, Program } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { Parser } from './index.js' */
import * as acorn from 'acorn';
import { walk } from 'zimmerframe';
import { tsPlugin } from '@sveltejs/acorn-typescript';
import * as e from '../../errors.js';
const ParserWithTS = acorn.Parser.extend(tsPlugin());
const JSParser = acorn.Parser;
const TSParser = JSParser.extend(tsPlugin());
/**
* @typedef {Comment & {
@ -20,15 +23,15 @@ const ParserWithTS = acorn.Parser.extend(tsPlugin());
* @param {boolean} [is_script]
*/
export function parse(source, comments, typescript, is_script) {
const parser = typescript ? ParserWithTS : acorn.Parser;
const acorn = typescript ? TSParser : JSParser;
const { onComment, add_comments } = get_comment_handlers(
source,
/** @type {CommentWithLocation[]} */ (comments)
);
// @ts-ignore
const parse_statement = parser.prototype.parseStatement;
// @ts-expect-error
const parse_statement = acorn.prototype.parseStatement;
// If we're dealing with a <script> then it might contain an export
// for something that doesn't exist directly inside but is inside the
@ -36,7 +39,7 @@ export function parse(source, comments, typescript, is_script) {
// an error in these cases
if (is_script) {
// @ts-ignore
parser.prototype.parseStatement = function (...args) {
acorn.prototype.parseStatement = function (...args) {
const v = parse_statement.call(this, ...args);
// @ts-ignore
this.undefinedExports = {};
@ -44,53 +47,77 @@ export function parse(source, comments, typescript, is_script) {
};
}
let ast;
try {
ast = parser.parse(source, {
const ast = acorn.parse(source, {
onComment,
sourceType: 'module',
ecmaVersion: 16,
locations: true
});
add_comments(ast);
return /** @type {Program} */ (ast);
} catch (err) {
// TODO the `return` in necessary for TS<7 due to a bug; otherwise
// the `finally` block is regarded as unreachable
return handle_parse_error(err);
} finally {
if (is_script) {
// @ts-ignore
parser.prototype.parseStatement = parse_statement;
// @ts-expect-error
acorn.prototype.parseStatement = parse_statement;
}
}
add_comments(ast);
return /** @type {Program} */ (ast);
}
/**
* @param {Parser} parser
* @param {string} source
* @param {Comment[]} comments
* @param {boolean} typescript
* @param {number} index
* @returns {acorn.Expression & { leadingComments?: CommentWithLocation[]; trailingComments?: CommentWithLocation[]; }}
*/
export function parse_expression_at(source, comments, typescript, index) {
const parser = typescript ? ParserWithTS : acorn.Parser;
export function parse_expression_at(parser, source, index) {
const acorn = parser.ts ? TSParser : JSParser;
const { onComment, add_comments } = get_comment_handlers(
source,
/** @type {CommentWithLocation[]} */ (comments),
index
);
const { onComment, add_comments } = get_comment_handlers(source, parser.root.comments, index);
const ast = parser.parseExpressionAt(source, index, {
onComment,
sourceType: 'module',
ecmaVersion: 16,
locations: true
});
try {
const ast = acorn.parseExpressionAt(source, index, {
onComment,
sourceType: 'module',
ecmaVersion: 16,
locations: true,
preserveParens: true
});
add_comments(ast);
return ast;
} catch (e) {
handle_parse_error(e);
}
}
const regex_position_indicator = / \(\d+:\d+\)$/;
add_comments(ast);
/**
* @param {any} err
* @returns {never}
*/
function handle_parse_error(err) {
e.js_parse_error(err.pos, err.message.replace(regex_position_indicator, ''));
}
return ast;
/**
* @param {acorn.Expression} node
* @returns {acorn.Expression}
*/
export function remove_parens(node) {
return walk(node, null, {
ParenthesizedExpression(node, context) {
return context.visit(node.expression);
}
});
}
/**

@ -4,7 +4,6 @@
// @ts-expect-error acorn type definitions are borked in the release we use
import { isIdentifierStart, isIdentifierChar } from 'acorn';
import fragment from './state/fragment.js';
import { regex_whitespace } from '../patterns.js';
import * as e from '../../errors.js';
import { create_fragment } from './utils/create.js';
import read_options from './read/options.js';
@ -12,7 +11,24 @@ import { is_reserved } from '../../../utils.js';
import { disallow_children } from '../2-analyze/visitors/shared/special-element.js';
import * as state from '../../state.js';
const regex_position_indicator = / \(\d+:\d+\)$/;
/** @param {number} cc */
function is_whitespace(cc) {
// fast path for common whitespace
if (cc === 32 || (cc <= 13 && cc >= 9)) return true;
// rare whitespace — \u00a0, \u1680, \u2000-\u200a, \u2028, \u2029, \u202f, \u205f, \u3000, \ufeff
if (cc < 160) return false;
return (
cc === 160 ||
cc === 5760 ||
(cc >= 8192 && cc <= 8202) ||
cc === 8232 ||
cc === 8233 ||
cc === 8239 ||
cc === 8287 ||
cc === 12288 ||
cc === 65279
);
}
const regex_lang_attribute =
/<!--[^]*?-->|<script\s+(?:[^>]*|(?:[^=>'"/]+=(?:"[^"]*"|'[^']*'|[^>\s]+)\s+)*)lang=(["'])?([^"' >]+)\1[^>]*>/g;
@ -157,14 +173,6 @@ export class Parser {
return this.stack[this.stack.length - 1];
}
/**
* @param {any} err
* @returns {never}
*/
acorn_error(err) {
e.js_parse_error(err.pos, err.message.replace(regex_position_indicator, ''));
}
/**
* @param {string} str
* @param {boolean} required
@ -191,22 +199,26 @@ export class Parser {
return this.template[this.index] === str;
}
return this.template.slice(this.index, this.index + length) === str;
return this.template.startsWith(str, this.index);
}
/**
* Match a regex at the current index
* @param {RegExp} pattern Should have a ^ anchor at the start so the regex doesn't search past the beginning, resulting in worse performance
* @param {RegExp} pattern Should have the sticky (`y`) flag so that it only matches at the current index
*/
match_regex(pattern) {
const match = pattern.exec(this.template.slice(this.index));
if (!match || match.index !== 0) return null;
pattern.lastIndex = this.index;
const match = pattern.exec(this.template);
if (!match || match.index !== this.index) return null;
return match[0];
}
allow_whitespace() {
while (this.index < this.template.length && regex_whitespace.test(this.template[this.index])) {
while (
this.index < this.template.length &&
is_whitespace(this.template.charCodeAt(this.index))
) {
this.index++;
}
}
@ -282,7 +294,7 @@ export class Parser {
}
require_whitespace() {
if (!regex_whitespace.test(this.template[this.index])) {
if (!is_whitespace(this.template.charCodeAt(this.index))) {
e.expected_whitespace(this.index);
}

@ -1,7 +1,7 @@
/** @import { Pattern } from 'estree' */
/** @import { Parser } from '../index.js' */
import { match_bracket } from '../utils/bracket.js';
import { parse_expression_at } from '../acorn.js';
import { parse_expression_at, remove_parens } from '../acorn.js';
import { regex_not_newline_characters } from '../../patterns.js';
import * as e from '../../../errors.js';
@ -35,38 +35,32 @@ export default function read_pattern(parser) {
const pattern_string = parser.template.slice(start, i);
try {
// the length of the `space_with_newline` has to be start - 1
// because we added a `(` in front of the pattern_string,
// which shifted the entire string to right by 1
// so we offset it by removing 1 character in the `space_with_newline`
// to achieve that, we remove the 1st space encountered,
// so it will not affect the `column` of the node
let space_with_newline = parser.template
.slice(0, start)
.replace(regex_not_newline_characters, ' ');
const first_space = space_with_newline.indexOf(' ');
space_with_newline =
space_with_newline.slice(0, first_space) + space_with_newline.slice(first_space + 1);
const expression = /** @type {any} */ (
parse_expression_at(
`${space_with_newline}(${pattern_string} = 1)`,
parser.root.comments,
parser.ts,
start - 1
)
).left;
expression.typeAnnotation = read_type_annotation(parser);
if (expression.typeAnnotation) {
expression.end = expression.typeAnnotation.end;
}
return expression;
} catch (error) {
parser.acorn_error(error);
// the length of the `space_with_newline` has to be start - 1
// because we added a `(` in front of the pattern_string,
// which shifted the entire string to right by 1
// so we offset it by removing 1 character in the `space_with_newline`
// to achieve that, we remove the 1st space encountered,
// so it will not affect the `column` of the node
let space_with_newline = parser.template
.slice(0, start)
.replace(regex_not_newline_characters, ' ');
const first_space = space_with_newline.indexOf(' ');
space_with_newline =
space_with_newline.slice(0, first_space) + space_with_newline.slice(first_space + 1);
/** @type {any} */
let expression = remove_parens(
parse_expression_at(parser, `${space_with_newline}(${pattern_string} = 1)`, start - 1)
);
expression = expression.left;
expression.typeAnnotation = read_type_annotation(parser);
if (expression.typeAnnotation) {
expression.end = expression.typeAnnotation.end;
}
return expression;
}
/**
@ -92,13 +86,13 @@ function read_type_annotation(parser) {
// parameters as part of a sequence expression instead, and will then error on optional
// parameters (`?:`). Therefore replace that sequence with something that will not error.
parser.template.slice(parser.index).replace(/\?\s*:/g, ':');
let expression = parse_expression_at(template, parser.root.comments, parser.ts, a);
let expression = remove_parens(parse_expression_at(parser, template, a));
// `foo: bar = baz` gets mangled — fix it
if (expression.type === 'AssignmentExpression') {
let b = expression.right.start;
while (template[b] !== '=') b -= 1;
expression = parse_expression_at(template.slice(0, b), parser.root.comments, parser.ts, a);
expression = remove_parens(parse_expression_at(parser, template.slice(0, b), a));
}
// `array as item: string, index` becomes `string, index`, which is mistaken as a sequence expression - fix that

@ -1,6 +1,6 @@
/** @import { Expression } from 'estree' */
/** @import { Parser } from '../index.js' */
import { parse_expression_at } from '../acorn.js';
import { parse_expression_at, remove_parens } from '../acorn.js';
import { regex_whitespace } from '../../patterns.js';
import * as e from '../../../errors.js';
import { find_matching_bracket } from '../utils/bracket.js';
@ -34,50 +34,16 @@ export function get_loose_identifier(parser, opening_token) {
*/
export default function read_expression(parser, opening_token, disallow_loose) {
try {
let comment_index = parser.root.comments.length;
const node = parse_expression_at(
parser.template,
parser.root.comments,
parser.ts,
parser.index
);
let num_parens = 0;
let i = parser.root.comments.length;
while (i-- > comment_index) {
const comment = parser.root.comments[i];
if (comment.end < node.start) {
parser.index = comment.end;
break;
}
}
for (let i = parser.index; i < /** @type {number} */ (node.start); i += 1) {
if (parser.template[i] === '(') num_parens += 1;
}
const node = parse_expression_at(parser, parser.template, parser.index);
let index = /** @type {number} */ (node.end);
const last_comment = parser.root.comments.at(-1);
if (last_comment && last_comment.end > index) index = last_comment.end;
while (num_parens > 0) {
const char = parser.template[index];
if (char === ')') {
num_parens -= 1;
} else if (!regex_whitespace.test(char)) {
e.expected_token(index, ')');
}
index += 1;
}
parser.index = index;
return /** @type {Expression} */ (node);
return /** @type {Expression} */ (remove_parens(node));
} catch (err) {
// If we are in an each loop we need the error to be thrown in cases like
// `as { y = z }` so we still throw and handle the error there
@ -88,6 +54,6 @@ export default function read_expression(parser, opening_token, disallow_loose) {
}
}
parser.acorn_error(err);
throw err;
}
}

@ -9,7 +9,7 @@ import { is_text_attribute } from '../../../utils/ast.js';
import { locator } from '../../../state.js';
const regex_closing_script_tag = /<\/script\s*>/;
const regex_starts_with_closing_script_tag = /^<\/script\s*>/;
const regex_starts_with_closing_script_tag = /<\/script\s*>/y;
const RESERVED_ATTRIBUTES = ['server', 'client', 'worker', 'test', 'default'];
const ALLOWED_ATTRIBUTES = ['context', 'generics', 'lang', 'module'];
@ -31,14 +31,7 @@ export function read_script(parser, start, attributes) {
parser.template.slice(0, script_start).replace(regex_not_newline_characters, ' ') + data;
parser.read(regex_starts_with_closing_script_tag);
/** @type {Program} */
let ast;
try {
ast = acorn.parse(source, parser.root.comments, parser.ts, true);
} catch (err) {
parser.acorn_error(err);
}
const ast = acorn.parse(source, parser.root.comments, parser.ts, true);
ast.start = script_start;

@ -2,17 +2,17 @@
/** @import { Parser } from '../index.js' */
import * as e from '../../../errors.js';
const REGEX_MATCHER = /^[~^$*|]?=/;
const REGEX_MATCHER = /[~^$*|]?=/y;
const REGEX_CLOSING_BRACKET = /[\s\]]/;
const REGEX_ATTRIBUTE_FLAGS = /^[a-zA-Z]+/; // only `i` and `s` are valid today, but make it future-proof
const REGEX_COMBINATOR = /^(\+|~|>|\|\|)/;
const REGEX_PERCENTAGE = /^\d+(\.\d+)?%/;
const REGEX_ATTRIBUTE_FLAGS = /[a-zA-Z]+/y; // only `i` and `s` are valid today, but make it future-proof
const REGEX_COMBINATOR = /(\+|~|>|\|\|)/y;
const REGEX_PERCENTAGE = /\d+(\.\d+)?%/y;
const REGEX_NTH_OF =
/^(even|odd|\+?(\d+|\d*n(\s*[+-]\s*\d+)?)|-\d*n(\s*\+\s*\d+))((?=\s*[,)])|\s+of\s+)/;
/(even|odd|\+?(\d+|\d*n(\s*[+-]\s*\d+)?)|-\d*n(\s*\+\s*\d+))((?=\s*[,)])|\s+of\s+)/y;
const REGEX_WHITESPACE_OR_COLON = /[\s:]/;
const REGEX_LEADING_HYPHEN_OR_DIGIT = /-?\d/;
const REGEX_LEADING_HYPHEN_OR_DIGIT = /-?\d/y;
const REGEX_VALID_IDENTIFIER_CHAR = /[a-zA-Z0-9_-]/;
const REGEX_UNICODE_SEQUENCE = /^\\[0-9a-fA-F]{1,6}(\r\n|\s)?/;
const REGEX_UNICODE_SEQUENCE = /\\[0-9a-fA-F]{1,6}(\r\n|\s)?/y;
const REGEX_COMMENT_CLOSE = /\*\//;
const REGEX_HTML_COMMENT_CLOSE = /-->/;
@ -28,7 +28,7 @@ export default function read_style(parser, start, attributes) {
const content_end = parser.index;
parser.eat('</style', true);
parser.read(/^\s*>/);
parser.read(/\s*>/y);
return {
type: 'StyleSheet',
@ -524,6 +524,21 @@ function read_value(parser) {
in_url = true;
} else if ((char === ';' || char === '{' || char === '}') && !in_url && !quote_mark) {
return value.trim();
} else if (
char === '/' &&
!in_url &&
!quote_mark &&
parser.template[parser.index + 1] === '*'
) {
parser.index += 2;
while (parser.index < parser.template.length) {
if (parser.template[parser.index] === '*' && parser.template[parser.index + 1] === '/') {
parser.index += 2;
break;
}
parser.index++;
}
continue;
}
value += char;

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

@ -2,7 +2,7 @@
/** @import { Location } from 'locate-character' */
/** @import { AST } from '#compiler' */
/** @import { Parser } from '../index.js' */
import { is_void } from '../../../../utils.js';
import { is_void, REGEX_VALID_TAG_NAME } from '../../../../utils.js';
import read_expression from '../read/expression.js';
import { read_script } from '../read/script.js';
import read_style from '../read/style.js';
@ -17,15 +17,25 @@ import { list } from '../../../utils/string.js';
import { locator } from '../../../state.js';
import * as b from '#compiler/builders';
const regex_invalid_unquoted_attribute_value = /^(\/>|[\s"'=<>`])/;
const regex_closing_textarea_tag = /^<\/textarea(\s[^>]*)?>/i;
const regex_invalid_unquoted_attribute_value = /(\/>|[\s"'=<>`])/y;
const regex_closing_textarea_tag = /<\/textarea(\s[^>]*)?>/iy;
const regex_closing_comment = /-->/;
const regex_whitespace_or_slash_or_closing_tag = /(\s|\/|>)/;
const regex_token_ending_character = /[\s=/>"']/;
const regex_starts_with_quote_characters = /^["']/;
const regex_attribute_value = /^(?:"([^"]*)"|'([^'])*'|([^>\s]+))/;
const regex_valid_element_name =
/^(?:![a-zA-Z]+|[a-zA-Z](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?|[a-zA-Z][a-zA-Z0-9]*:[a-zA-Z][a-zA-Z0-9-]*[a-zA-Z0-9])$/;
const regex_starts_with_quote_characters = /["']/y;
const regex_attribute_value = /(?:"([^"]*)"|'([^'])*'|([^>\s]+))/y;
const regex_doctype_name = /^![a-zA-Z]+$/;
const regex_namespaced_name = /^[a-zA-Z][a-zA-Z0-9]*:[a-zA-Z][a-zA-Z0-9-]*[a-zA-Z0-9]$/;
/** @param {string} name */
function is_valid_element_name(name) {
// DOCTYPE (e.g. !DOCTYPE)
if (regex_doctype_name.test(name)) return true;
// svelte:* meta tags (e.g. svelte:element, svelte:head)
if (regex_namespaced_name.test(name)) return true;
// standard HTML/SVG/MathML elements and custom elements
return REGEX_VALID_TAG_NAME.test(name);
}
export const regex_valid_component_name =
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Lexical_grammar#identifiers adjusted for our needs
// (must start with uppercase letter if no dots, can contain dots)
@ -134,7 +144,7 @@ export default function element(parser) {
e.svelte_meta_invalid_tag(bounds, list(Array.from(meta_tags.keys())));
}
if (!regex_valid_element_name.test(tag.name) && !regex_valid_component_name.test(tag.name)) {
if (!is_valid_element_name(tag.name) && !regex_valid_component_name.test(tag.name)) {
// <div. -> in the middle of typing -> allow in loose mode
if (!parser.loose || !tag.name.endsWith('.')) {
const bounds = { start: start + 1, end: start + 1 + tag.name.length };
@ -392,7 +402,10 @@ export default function element(parser) {
// special case
element.fragment.nodes = read_sequence(
parser,
() => regex_closing_textarea_tag.test(parser.template.slice(parser.index)),
() => {
regex_closing_textarea_tag.lastIndex = parser.index;
return regex_closing_textarea_tag.test(parser.template);
},
'inside <textarea>'
);
parser.read(regex_closing_textarea_tag);
@ -400,7 +413,13 @@ export default function element(parser) {
} else if (tag.name === 'script' || tag.name === 'style') {
// special case
const start = parser.index;
const data = parser.read_until(new RegExp(`</${tag.name}>`));
const close_tag = `</${tag.name}>`;
const close_index = parser.template.indexOf(close_tag, parser.index);
const data = parser.template.slice(
parser.index,
close_index === -1 ? parser.template.length : close_index
);
parser.index = close_index === -1 ? parser.template.length : close_index;
const end = parser.index;
/** @type {AST.Text} */
@ -499,6 +518,15 @@ function read_static_attribute(parser) {
* @returns {AST.Attribute | AST.SpreadAttribute | AST.Directive | AST.AttachTag | null}
*/
function read_attribute(parser) {
/** @type {AST.JSComment | null} */
// eslint-disable-next-line no-useless-assignment -- it is, in fact, eslint that is useless
let comment = null;
while ((comment = read_comment(parser))) {
parser.root.comments.push(comment);
parser.allow_whitespace();
}
const start = parser.index;
if (parser.eat('{')) {
@ -695,6 +723,50 @@ function read_attribute(parser) {
return create_attribute(tag.name, tag.loc, start, end, value);
}
/**
* @param {Parser} parser
* @returns {AST.JSComment | null}
*/
function read_comment(parser) {
const start = parser.index;
if (parser.eat('//')) {
const value = parser.read_until(/\n/);
const end = parser.index;
return {
type: 'Line',
start,
end,
value,
loc: {
start: locator(start),
end: locator(end)
}
};
}
if (parser.eat('/*')) {
const value = parser.read_until(/\*\//);
parser.eat('*/');
const end = parser.index;
return {
type: 'Block',
start,
end,
value,
loc: {
start: locator(start),
end: locator(end)
}
};
}
return null;
}
/**
* @param {string} name
* @returns {any}
@ -789,7 +861,8 @@ function read_sequence(parser, done, location) {
/** @param {number} end */
function flush(end) {
if (current_chunk.raw) {
if (end > current_chunk.start) {
current_chunk.raw = parser.template.slice(current_chunk.start, end);
current_chunk.data = decode_character_references(current_chunk.raw, true);
current_chunk.end = end;
chunks.push(current_chunk);
@ -843,7 +916,7 @@ function read_sequence(parser, done, location) {
data: ''
};
} else {
current_chunk.raw += parser.template[parser.index++];
parser.index++;
}
}

@ -10,7 +10,7 @@ import read_expression, { get_loose_identifier } from '../read/expression.js';
import { create_fragment } from '../utils/create.js';
import { match_bracket } from '../utils/bracket.js';
const regex_whitespace_with_closing_curly_brace = /^\s*}/;
const regex_whitespace_with_closing_curly_brace = /\s*}/y;
const pointy_bois = { '<': '>' };
@ -392,12 +392,7 @@ function open(parser) {
let function_expression = matched
? /** @type {ArrowFunctionExpression} */ (
parse_expression_at(
prelude + `${params} => {}`,
parser.root.comments,
parser.ts,
params_start
)
parse_expression_at(parser, prelude + `${params} => {}`, params_start)
)
: { params: [] };

@ -6,12 +6,12 @@ import { decode_character_references } from '../utils/html.js';
export default function text(parser) {
const start = parser.index;
let data = '';
while (parser.index < parser.template.length && !parser.match('<') && !parser.match('{')) {
data += parser.template[parser.index++];
parser.index++;
}
const data = parser.template.slice(start, parser.index);
/** @type {AST.Text} */
parser.append({
type: 'Text',

@ -141,13 +141,15 @@ const default_brackets = {
'[': ']'
};
const default_close = new Set(Object.values(default_brackets));
/**
* @param {Parser} parser
* @param {number} start
* @param {Record<string, string>} brackets
*/
export function match_bracket(parser, start, brackets = default_brackets) {
const close = Object.values(brackets);
const close = brackets === default_brackets ? default_close : new Set(Object.values(brackets));
const bracket_stack = [];
let i = start;
@ -162,7 +164,7 @@ export function match_bracket(parser, start, brackets = default_brackets) {
if (char in brackets) {
bracket_stack.push(char);
} else if (close.includes(char)) {
} else if (close.has(char)) {
const popped = /** @type {string} */ (bracket_stack.pop());
const expected = /** @type {string} */ (brackets[popped]);

@ -22,6 +22,50 @@ const whitelist_attribute_selector = new Map([
['dialog', ['open']]
]);
/**
* HTML attributes whose enumerated values are case-insensitive per the HTML spec.
* CSS attribute selectors match these values case-insensitively in HTML documents.
* @see {@link https://html.spec.whatwg.org/multipage/semantics-other.html#case-sensitivity-of-selectors HTML spec}
*/
const case_insensitive_attributes = new Set([
'accept-charset',
'autocapitalize',
'autocomplete',
'behavior',
'charset',
'crossorigin',
'decoding',
'dir',
'direction',
'draggable',
'enctype',
'enterkeyhint',
'fetchpriority',
'formenctype',
'formmethod',
'formtarget',
'hidden',
'http-equiv',
'inputmode',
'kind',
'loading',
'method',
'preload',
'referrerpolicy',
'rel',
'rev',
'role',
'rules',
'scope',
'shape',
'spellcheck',
'target',
'translate',
'type',
'valign',
'wrap'
]);
/** @type {Compiler.AST.CSS.Combinator} */
const descendant_combinator = {
type: 'Combinator',
@ -81,9 +125,9 @@ const seen = new Set();
/**
*
* @param {Compiler.AST.CSS.StyleSheet} stylesheet
* @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement} element
* @param {Iterable<Compiler.AST.RegularElement | Compiler.AST.SvelteElement>} elements
*/
export function prune(stylesheet, element) {
export function prune(stylesheet, elements) {
walk(/** @type {Compiler.AST.CSS.Node} */ (stylesheet), null, {
Rule(node, context) {
if (node.metadata.is_global_block) {
@ -95,17 +139,19 @@ export function prune(stylesheet, element) {
ComplexSelector(node) {
const selectors = get_relative_selectors(node);
seen.clear();
if (
apply_selector(
selectors,
/** @type {Compiler.AST.CSS.Rule} */ (node.metadata.rule),
element,
BACKWARD
)
) {
node.metadata.used = true;
for (const element of elements) {
seen.clear();
if (
apply_selector(
selectors,
/** @type {Compiler.AST.CSS.Rule} */ (node.metadata.rule),
element,
BACKWARD
)
) {
node.metadata.used = true;
}
}
// note: we don't call context.next() here, we only recurse into
@ -190,16 +236,36 @@ function truncate(node) {
* @param {Compiler.AST.CSS.Rule} rule
* @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement} element
* @param {Direction} direction
* @param {number} [from]
* @param {number} [to]
* @returns {boolean}
*/
function apply_selector(relative_selectors, rule, element, direction) {
const rest_selectors = relative_selectors.slice();
const relative_selector = direction === FORWARD ? rest_selectors.shift() : rest_selectors.pop();
function apply_selector(
relative_selectors,
rule,
element,
direction,
from = 0,
to = relative_selectors.length
) {
if (from >= to) return false;
const selector_index = direction === FORWARD ? from : to - 1;
const relative_selector = relative_selectors[selector_index];
const rest_from = direction === FORWARD ? from + 1 : from;
const rest_to = direction === FORWARD ? to : to - 1;
const matched =
!!relative_selector &&
relative_selector_might_apply_to_node(relative_selector, rule, element, direction) &&
apply_combinator(relative_selector, rest_selectors, rule, element, direction);
apply_combinator(
relative_selector,
relative_selectors,
rest_from,
rest_to,
rule,
element,
direction
);
if (matched) {
if (!is_outer_global(relative_selector)) {
@ -214,15 +280,21 @@ function apply_selector(relative_selectors, rule, element, direction) {
/**
* @param {Compiler.AST.CSS.RelativeSelector} relative_selector
* @param {Compiler.AST.CSS.RelativeSelector[]} rest_selectors
* @param {Compiler.AST.CSS.RelativeSelector[]} relative_selectors
* @param {number} from
* @param {number} to
* @param {Compiler.AST.CSS.Rule} rule
* @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement | Compiler.AST.RenderTag | Compiler.AST.Component | Compiler.AST.SvelteComponent | Compiler.AST.SvelteSelf} node
* @param {Direction} direction
* @returns {boolean}
*/
function apply_combinator(relative_selector, rest_selectors, rule, node, direction) {
function apply_combinator(relative_selector, relative_selectors, from, to, rule, node, direction) {
const combinator =
direction == FORWARD ? rest_selectors[0]?.combinator : relative_selector.combinator;
direction == FORWARD
? from < to
? relative_selectors[from].combinator
: undefined
: relative_selector.combinator;
if (!combinator) return true;
switch (combinator.name) {
@ -236,7 +308,7 @@ function apply_combinator(relative_selector, rest_selectors, rule, node, directi
let parent_matched = false;
for (const parent of parents) {
if (apply_selector(rest_selectors, rule, parent, direction)) {
if (apply_selector(relative_selectors, rule, parent, direction, from, to)) {
parent_matched = true;
}
}
@ -245,7 +317,7 @@ function apply_combinator(relative_selector, rest_selectors, rule, node, directi
parent_matched ||
(direction === BACKWARD &&
(!is_adjacent || parents.length === 0) &&
rest_selectors.every((selector) => is_global(selector, rule)))
every_is_global(relative_selectors, from, to, rule))
);
}
@ -262,10 +334,12 @@ function apply_combinator(relative_selector, rest_selectors, rule, node, directi
possible_sibling.type === 'Component'
) {
// `{@render foo()}<p>foo</p>` with `:global(.x) + p` is a match
if (rest_selectors.length === 1 && rest_selectors[0].metadata.is_global) {
if (to - from === 1 && relative_selectors[from].metadata.is_global) {
sibling_matched = true;
}
} else if (apply_selector(rest_selectors, rule, possible_sibling, direction)) {
} else if (
apply_selector(relative_selectors, rule, possible_sibling, direction, from, to)
) {
sibling_matched = true;
}
}
@ -274,7 +348,7 @@ function apply_combinator(relative_selector, rest_selectors, rule, node, directi
sibling_matched ||
(direction === BACKWARD &&
get_element_parent(node) === null &&
rest_selectors.every((selector) => is_global(selector, rule)))
every_is_global(relative_selectors, from, to, rule))
);
}
@ -284,6 +358,20 @@ function apply_combinator(relative_selector, rest_selectors, rule, node, directi
}
}
/**
* @param {Compiler.AST.CSS.RelativeSelector[]} relative_selectors
* @param {number} from
* @param {number} to
* @param {Compiler.AST.CSS.Rule} rule
* @returns {boolean}
*/
function every_is_global(relative_selectors, from, to, rule) {
for (let i = from; i < to; i++) {
if (!is_global(relative_selectors[i], rule)) return false;
}
return true;
}
/**
* Returns `true` if the relative selector is global, meaning
* it's a `:global(...)` or unscopeable selector, or
@ -346,42 +434,37 @@ const regex_backslash_and_following_character = /\\(.)/g;
* @returns {boolean}
*/
function relative_selector_might_apply_to_node(relative_selector, rule, element, direction) {
// Sort :has(...) selectors in one bucket and everything else into another
const has_selectors = [];
const other_selectors = [];
/** @type {boolean | undefined} */
let include_self;
for (const selector of relative_selector.selectors) {
// Handle :has(...) selectors inline to avoid allocating temporary arrays
if (selector.type === 'PseudoClassSelector' && selector.name === 'has' && selector.args) {
has_selectors.push(selector);
} else {
other_selectors.push(selector);
}
}
// If we're called recursively from a :has(...) selector, we're on the way of checking if the other selectors match.
// In that case ignore this check (because we just came from this) to avoid an infinite loop.
if (has_selectors.length > 0) {
// If this is a :has inside a global selector, we gotta include the element itself, too,
// because the global selector might be for an element that's outside the component,
// e.g. :root:has(.scoped), :global(.foo):has(.scoped), or :root { &:has(.scoped) {} }
const rules = get_parent_rules(rule);
const include_self =
rules.some((r) => r.prelude.children.some((c) => c.children.some((s) => is_global(s, r)))) ||
rules[rules.length - 1].prelude.children.some((c) =>
c.children.some((r) =>
r.selectors.some(
(s) =>
s.type === 'PseudoClassSelector' &&
(s.name === 'root' || (s.name === 'global' && s.args))
)
)
);
// Lazy-compute include_self on first :has encounter
if (include_self === undefined) {
// If this is a :has inside a global selector, we gotta include the element itself, too,
// because the global selector might be for an element that's outside the component,
// e.g. :root:has(.scoped), :global(.foo):has(.scoped), or :root { &:has(.scoped) {} }
const rules = get_parent_rules(rule);
include_self =
rules.some((r) =>
r.prelude.children.some((c) => c.children.some((s) => is_global(s, r)))
) ||
rules[rules.length - 1].prelude.children.some((c) =>
c.children.some((r) =>
r.selectors.some(
(s) =>
s.type === 'PseudoClassSelector' &&
(s.name === 'root' || (s.name === 'global' && s.args))
)
)
);
}
// :has(...) is special in that it means "look downwards in the CSS tree". Since our matching algorithm goes
// upwards and back-to-front, we need to first check the selectors inside :has(...), then check the rest of the
// selector in a way that is similar to ancestor matching. In a sense, we're treating `.x:has(.y)` as `.x .y`.
for (const has_selector of has_selectors) {
const complex_selectors = /** @type {Compiler.AST.CSS.SelectorList} */ (has_selector.args)
// :has(...) is special in that it means "look downwards in the CSS tree". Since our matching algorithm goes
// upwards and back-to-front, we need to first check the selectors inside :has(...), then check the rest of the
// selector in a way that is similar to ancestor matching. In a sense, we're treating `.x:has(.y)` as `.x .y`.
const complex_selectors = /** @type {Compiler.AST.CSS.SelectorList} */ (selector.args)
.children;
let matched = false;
@ -419,13 +502,15 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element,
if (!matched) {
return false;
}
continue;
}
}
for (const selector of other_selectors) {
if (selector.type === 'Percentage' || selector.type === 'Nth') continue;
const name = selector.name.replace(regex_backslash_and_following_character, '$1');
const name = selector.name.includes('\\')
? selector.name.replace(regex_backslash_and_following_character, '$1')
: selector.name;
switch (selector.type) {
case 'PseudoClassSelector': {
@ -523,7 +608,9 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element,
selector.name,
selector.value && unquote(selector.value),
selector.matcher,
selector.flags?.includes('i') ?? false
(selector.flags?.includes('i') ?? false) ||
(!selector.flags?.includes('s') &&
case_insensitive_attributes.has(selector.name.toLowerCase()))
)
) {
return false;
@ -624,11 +711,11 @@ function test_attribute(operator, expected_value, case_insensitive, value) {
* @param {boolean} case_insensitive
*/
function attribute_matches(node, name, expected_value, operator, case_insensitive) {
const name_lower = name.toLowerCase();
for (const attribute of node.attributes) {
if (attribute.type === 'SpreadAttribute') return true;
if (attribute.type === 'BindDirective' && attribute.name === name) return true;
const name_lower = name.toLowerCase();
// match attributes against the corresponding directive but bail out on exact matching
if (attribute.type === 'StyleDirective' && name_lower === 'style') return true;
if (attribute.type === 'ClassDirective' && name_lower === 'class') {

@ -21,7 +21,7 @@ import { prune } from './css/css-prune.js';
import { hash, is_rune } from '../../../utils.js';
import { warn_unused } from './css/css-warn.js';
import { extract_svelte_ignore } from '../../utils/extract_svelte_ignore.js';
import { ignore_map, ignore_stack, pop_ignore, push_ignore } from '../../state.js';
import { ignore_map, get_ignore_snapshot, pop_ignore, push_ignore } from '../../state.js';
import { ArrowFunctionExpression } from './visitors/ArrowFunctionExpression.js';
import { AssignmentExpression } from './visitors/AssignmentExpression.js';
import { AnimateDirective } from './visitors/AnimateDirective.js';
@ -134,7 +134,7 @@ const visitors = {
push_ignore(ignores);
}
ignore_map.set(node, structuredClone(ignore_stack));
ignore_map.set(node, get_ignore_snapshot());
const scope = state.scopes.get(node);
next(scope !== undefined && scope !== state.scope ? { ...state, scope } : state);
@ -345,6 +345,8 @@ export function analyze_component(root, source, options) {
let synthetic_stores_legacy_check = [];
const runes_option = options.runes?.({ filename: options.filename });
// create synthetic bindings for store subscriptions
for (const [name, references] of module.scope.references) {
if (name[0] !== '$' || RESERVED.includes(name)) continue;
@ -359,7 +361,7 @@ export function analyze_component(root, source, options) {
// If we're not in legacy mode through the compiler option, assume the user
// is referencing a rune and not a global store.
if (
options.runes === false ||
runes_option === false ||
!is_rune(name) ||
(declaration !== null &&
// const state = $state(0) is valid
@ -395,7 +397,7 @@ export function analyze_component(root, source, options) {
e.store_invalid_scoped_subscription(is_nested_store_subscription_node);
}
if (options.runes !== false) {
if (runes_option !== false) {
if (declaration === null && /[a-z]/.test(store_name[0])) {
e.global_reference_invalid(references[0].node, name);
} else if (declaration !== null && is_rune(name)) {
@ -447,7 +449,7 @@ export function analyze_component(root, source, options) {
const component_name = get_component_name(options.filename);
const runes =
options.runes ??
runes_option ??
(has_await || instance.has_await || Array.from(module.scope.references.keys()).some(is_rune));
if (!runes) {
@ -463,7 +465,10 @@ export function analyze_component(root, source, options) {
}
}
const is_custom_element = !!options.customElementOptions || options.customElement;
const custom_element_from_option = options.customElement({ filename: options.filename });
const css = options.css({ filename: options.filename });
const custom_element = options.customElementOptions ?? custom_element_from_option;
const is_custom_element = !!options.customElementOptions || custom_element_from_option;
const name = module.scope.generate(options.name ?? component_name);
@ -491,7 +496,7 @@ export function analyze_component(root, source, options) {
maybe_runes:
!runes &&
// if they explicitly disabled runes, use the legacy behavior
options.runes !== false &&
runes_option !== false &&
![...module.scope.references.keys()].some((name) =>
['$$props', '$$restProps'].includes(name)
) &&
@ -523,8 +528,8 @@ export function analyze_component(root, source, options) {
needs_props: false,
event_directive_node: null,
uses_event_attributes: false,
custom_element: is_custom_element,
inject_styles: options.css === 'injected' || is_custom_element,
custom_element,
inject_styles: css === 'injected' || is_custom_element,
accessors:
is_custom_element ||
(runes ? false : !!options.accessors) ||
@ -680,7 +685,7 @@ export function analyze_component(root, source, options) {
w.options_deprecated_accessors(attribute);
}
if (attribute.name === 'customElement' && !options.customElement) {
if (attribute.name === 'customElement' && !custom_element_from_option) {
w.options_missing_custom_element(attribute);
}
@ -690,7 +695,7 @@ export function analyze_component(root, source, options) {
}
}
calculate_blockers(instance, scopes, analysis);
calculate_blockers(instance, analysis);
if (analysis.runes) {
const props_refs = module.scope.references.get('$$props');
@ -856,9 +861,7 @@ export function analyze_component(root, source, options) {
analyze_css(analysis.css.ast, analysis);
// mark nodes as scoped/unused/empty etc
for (const node of analysis.elements) {
prune(analysis.css.ast, node);
}
prune(analysis.css.ast, analysis.elements);
const { comment } = analysis.css.ast.content;
const should_ignore_unused =
@ -940,11 +943,10 @@ export function analyze_component(root, source, options) {
* top level statements. This includes indirect blockers such as functions referencing async top level statements.
*
* @param {Js} instance
* @param {Map<AST.SvelteNode, Scope>} scopes
* @param {ComponentAnalysis} analysis
* @returns {void}
*/
function calculate_blockers(instance, scopes, analysis) {
function calculate_blockers(instance, analysis) {
/**
* @param {ESTree.Node} expression
* @param {Scope} scope
@ -959,6 +961,14 @@ function calculate_blockers(instance, scopes, analysis) {
expression,
{ scope },
{
_(node, context) {
const scope = instance.scopes.get(node);
if (scope) {
context.next({ scope });
} else {
context.next();
}
},
ImportDeclaration(node) {},
Identifier(node, context) {
const parent = /** @type {ESTree.Node} */ (context.path.at(-1));
@ -979,14 +989,11 @@ function calculate_blockers(instance, scopes, analysis) {
/**
* @param {ESTree.Node} node
* @param {Set<ESTree.Node>} seen
* @param {Set<Binding>} reads
* @param {Set<Binding>} writes
* @param {Scope} scope
*/
const trace_references = (node, reads, writes, seen = new Set()) => {
if (seen.has(node)) return;
seen.add(node);
const trace_references = (node, reads, writes, scope) => {
/**
* @param {ESTree.Pattern} node
* @param {Scope} scope
@ -1005,10 +1012,10 @@ function calculate_blockers(instance, scopes, analysis) {
walk(
node,
{ scope: instance.scope },
{ scope },
{
_(node, context) {
const scope = scopes.get(node);
const scope = instance.scopes.get(node);
if (scope) {
context.next({ scope });
} else {
@ -1040,10 +1047,6 @@ function calculate_blockers(instance, scopes, analysis) {
writes.add(b);
}
},
// don't look inside functions until they are called
ArrowFunctionExpression(_, context) {},
FunctionDeclaration(_, context) {},
FunctionExpression(_, context) {},
Identifier(node, context) {
const parent = /** @type {ESTree.Node} */ (context.path.at(-1));
if (is_reference(node, parent)) {
@ -1052,13 +1055,28 @@ function calculate_blockers(instance, scopes, analysis) {
reads.add(binding);
}
}
}
},
ReturnStatement(node, context) {
// We have to assume that anything returned from a function, even if it's a function itself,
// might be called immediately, so we have to touch all references within it. Example:
// function foo() { return () => blocker; } foo(); // blocker is touched
if (node.argument) {
touch(node.argument, context.state.scope, reads);
}
},
// don't look inside functions until they are called
ArrowFunctionExpression(_, context) {},
FunctionDeclaration(_, context) {},
FunctionExpression(_, context) {}
}
);
};
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');
@ -1073,6 +1091,13 @@ function calculate_blockers(instance, scopes, 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>}
@ -1132,7 +1157,10 @@ function calculate_blockers(instance, scopes, analysis) {
/** @type {Set<Binding>} */
const writes = new Set();
trace_references(declarator, reads, writes);
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)
@ -1146,11 +1174,12 @@ function calculate_blockers(instance, scopes, 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) {
@ -1160,7 +1189,10 @@ function calculate_blockers(instance, scopes, analysis) {
/** @type {Set<Binding>} */
const writes = new Set();
trace_references(node, reads, writes);
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)
@ -1172,24 +1204,34 @@ function calculate_blockers(instance, scopes, 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();
const body =
const init =
fn.type === 'VariableDeclarator'
? /** @type {ESTree.FunctionExpression | ESTree.ArrowFunctionExpression} */ (fn.init).body
: fn.body;
trace_references(body, reads_writes, reads_writes);
? /** @type {ESTree.FunctionExpression | ESTree.ArrowFunctionExpression} */ (fn.init)
: fn;
trace_references(
init.body,
reads_writes,
reads_writes,
/** @type {Scope} */ (instance.scopes.get(init))
);
const max = [...reads_writes].reduce((max, binding) => {
if (binding.blocker) {

@ -2,6 +2,7 @@ import type { Scope } from '../scope.js';
import type { ComponentAnalysis, ReactiveStatement } from '../types.js';
import type { AST, StateField, ValidatedCompileOptions } from '#compiler';
import type { ExpressionMetadata } from '../nodes.js';
import type { Identifier } from 'estree';
export interface AnalysisState {
scope: Scope;
@ -33,6 +34,13 @@ export interface AnalysisState {
* Set when we're inside a `$derived(...)` expression (but not `$derived.by(...)`) or `@const`
*/
derived_function_depth: number;
/** Collected info about async `{@const }` declarations */
async_consts?: {
id: Identifier;
/** How many `$.run(...)` entries are already allocated in this scope */
declaration_count: number;
};
}
export type Context<State extends AnalysisState = AnalysisState> = import('zimmerframe').Context<

@ -1,6 +1,7 @@
/** @import { AST } from '#compiler' */
/** @import { Context } from '../types' */
import * as e from '../../../errors.js';
import * as b from '#compiler/builders';
import { validate_opening_tag } from './shared/utils.js';
/**
@ -42,4 +43,29 @@ export function ConstTag(node, context) {
function_depth: context.state.function_depth + 1,
derived_function_depth: context.state.function_depth + 1
});
const has_await = node.metadata.expression.has_await;
const blockers = [...node.metadata.expression.dependencies]
.map((dep) => dep.blocker)
.filter((b) => b !== null && b.object !== context.state.async_consts?.id);
if (has_await || context.state.async_consts || blockers.length > 0) {
const run = (context.state.async_consts ??= {
id: context.state.analysis.root.unique('promises'),
declaration_count: 0
});
node.metadata.promises_id = run.id;
const bindings = context.state.scope.get_bindings(declaration);
// keep the counter in sync with the number of thunks pushed in ConstTag in transform
// TODO 6.0 once non-async and non-runes mode is gone investigate making this more robust
// via something like the approach in https://github.com/sveltejs/svelte/pull/18032
const length = run.declaration_count + (blockers.length > 0 ? 1 : 0);
run.declaration_count += blockers.length > 0 ? 2 : 1;
const blocker = b.member(run.id, b.literal(length), true);
for (const binding of bindings) {
binding.blocker = blocker;
}
}
}

@ -6,5 +6,5 @@
* @param {Context} context
*/
export function Fragment(node, context) {
context.next({ ...context.state, fragment: node });
context.next({ ...context.state, fragment: node, async_consts: undefined });
}

@ -115,7 +115,8 @@ export function Identifier(node, context) {
!should_proxy(binding.initial.arguments[0], context.state.scope)))) ||
binding.kind === 'raw_state' ||
binding.kind === 'derived' ||
binding.kind === 'prop') &&
binding.kind === 'prop' ||
binding.kind === 'rest_prop') &&
// We're only concerned with reads here
(parent.type !== 'AssignmentExpression' || parent.left !== node) &&
parent.type !== 'UpdateExpression'

@ -16,6 +16,8 @@ import { regex_starts_with_newline } from '../../patterns.js';
import { check_element } from './shared/a11y/index.js';
import { validate_element } from './shared/element.js';
import { mark_subtree_dynamic } from './shared/fragment.js';
import { object } from '../../../utils/ast.js';
import { runes } from '../../../state.js';
/**
* @param {AST.RegularElement} node
@ -64,6 +66,34 @@ export function RegularElement(node, context) {
}
}
// Special case: `<select bind:value={foo}><option>{bar}</option>`
// means we need to invalidate `bar` whenever `foo` is mutated
if (node.name === 'select' && !runes) {
for (const attribute of node.attributes) {
if (
attribute.type === 'BindDirective' &&
attribute.name === 'value' &&
attribute.expression.type !== 'SequenceExpression'
) {
const identifier = object(attribute.expression);
const binding = identifier && context.state.scope.get(identifier.name);
if (binding) {
for (const name of context.state.scope.references.keys()) {
if (name === binding.node.name) continue;
const indirect = context.state.scope.get(name);
if (indirect) {
binding.legacy_indirect_bindings.add(indirect);
}
}
}
break;
}
}
}
// Special case: single expression tag child of option element -> add "fake" attribute
// to ensure that value types are the same (else for example numbers would be strings)
if (

@ -100,6 +100,11 @@ export function check_element(node, context) {
}
}
const interactivity = element_interactivity(node.name, attribute_map);
const is_interactive = interactivity === ElementInteractivity.Interactive;
const is_non_interactive = interactivity === ElementInteractivity.NonInteractive;
const is_static = interactivity === ElementInteractivity.Static;
for (const attribute of node.attributes) {
if (attribute.type !== 'Attribute') continue;
@ -133,7 +138,7 @@ export function check_element(node, context) {
if (
name === 'aria-activedescendant' &&
!is_dynamic_element &&
!is_interactive_element(node.name, attribute_map) &&
!is_interactive &&
!attribute_map.has('tabindex') &&
!has_spread
) {
@ -215,7 +220,7 @@ export function check_element(node, context) {
!is_hidden_from_screen_reader(node.name, attribute_map) &&
!is_presentation_role(current_role) &&
is_interactive_roles(current_role) &&
is_static_element(node.name, attribute_map) &&
is_static &&
!attribute_map.get('tabindex')
) {
const has_interactive_handlers = [...handlers].some((handler) =>
@ -229,7 +234,7 @@ export function check_element(node, context) {
// no-interactive-element-to-noninteractive-role
if (
!has_spread &&
is_interactive_element(node.name, attribute_map) &&
is_interactive &&
(is_non_interactive_roles(current_role) || is_presentation_role(current_role))
) {
w.a11y_no_interactive_element_to_noninteractive_role(node, node.name, current_role);
@ -238,7 +243,7 @@ export function check_element(node, context) {
// no-noninteractive-element-to-interactive-role
if (
!has_spread &&
is_non_interactive_element(node.name, attribute_map) &&
is_non_interactive &&
is_interactive_roles(current_role) &&
!a11y_non_interactive_element_to_interactive_role_exceptions[node.name]?.includes(
current_role
@ -291,13 +296,13 @@ export function check_element(node, context) {
!is_dynamic_element &&
!is_hidden_from_screen_reader(node.name, attribute_map) &&
(!role || is_non_presentation_role) &&
!is_interactive_element(node.name, attribute_map) &&
!is_interactive &&
!has_spread
) {
const has_key_event =
handlers.has('keydown') || handlers.has('keyup') || handlers.has('keypress');
if (!has_key_event) {
w.a11y_click_events_have_key_events(node);
w.a11y_click_events_have_key_events(node, node.name);
}
}
}
@ -307,11 +312,7 @@ export function check_element(node, context) {
);
// no-noninteractive-tabindex
if (
!is_dynamic_element &&
!is_interactive_element(node.name, attribute_map) &&
!is_interactive_roles(role_static_value)
) {
if (!is_dynamic_element && !is_interactive && !is_interactive_roles(role_static_value)) {
const tab_index = attribute_map.get('tabindex');
const tab_index_value = get_static_text_value(tab_index);
if (tab_index && (tab_index_value === null || Number(tab_index_value) >= 0)) {
@ -341,9 +342,8 @@ export function check_element(node, context) {
!has_contenteditable_attr &&
!is_hidden_from_screen_reader(node.name, attribute_map) &&
!is_presentation_role(role_static_value) &&
((!is_interactive_element(node.name, attribute_map) &&
is_non_interactive_roles(role_static_value)) ||
(is_non_interactive_element(node.name, attribute_map) && !role))
((!is_interactive && is_non_interactive_roles(role_static_value)) ||
(is_non_interactive && !role))
) {
const has_interactive_handlers = [...handlers].some((handler) =>
a11y_recommended_interactive_handlers.includes(handler)
@ -359,9 +359,9 @@ export function check_element(node, context) {
(!role || role_static_value !== null) &&
!is_hidden_from_screen_reader(node.name, attribute_map) &&
!is_presentation_role(role_static_value) &&
!is_interactive_element(node.name, attribute_map) &&
!is_interactive &&
!is_interactive_roles(role_static_value) &&
!is_non_interactive_element(node.name, attribute_map) &&
!is_non_interactive &&
!is_non_interactive_roles(role_static_value) &&
!is_abstract_role(role_static_value)
) {
@ -643,33 +643,6 @@ function element_interactivity(tag_name, attribute_map) {
return ElementInteractivity.Static;
}
/**
* @param {string} tag_name
* @param {Map<string, AST.Attribute>} attribute_map
* @returns {boolean}
*/
function is_interactive_element(tag_name, attribute_map) {
return element_interactivity(tag_name, attribute_map) === ElementInteractivity.Interactive;
}
/**
* @param {string} tag_name
* @param {Map<string, AST.Attribute>} attribute_map
* @returns {boolean}
*/
function is_non_interactive_element(tag_name, attribute_map) {
return element_interactivity(tag_name, attribute_map) === ElementInteractivity.NonInteractive;
}
/**
* @param {string} tag_name
* @param {Map<string, AST.Attribute>} attribute_map
* @returns {boolean}
*/
function is_static_element(tag_name, attribute_map) {
return element_interactivity(tag_name, attribute_map) === ElementInteractivity.Static;
}
/**
* @param {ARIARoleDefinitionKey} role
* @param {string} tag_name

@ -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)];
@ -595,7 +595,7 @@ export function client_component(analysis, options) {
);
}
const ce = options.customElementOptions ?? options.customElement;
const ce = analysis.custom_element;
if (ce) {
const ce_props = typeof ce === 'boolean' ? {} : ce.props || {};

@ -30,13 +30,15 @@ export class Template {
/**
* @param {string} name
* @param {number} start
* @param {boolean} is_html
*/
push_element(name, start) {
push_element(name, start, is_html) {
this.#element = {
type: 'element',
name,
attributes: {},
children: [],
is_html,
start
};
@ -100,7 +102,7 @@ function stringify(item) {
for (const key in item.attributes) {
const value = item.attributes[key];
str += ` ${key}`;
str += ` ${item.is_html ? key.toLowerCase() : key}`;
if (value !== undefined) str += `="${escape_html(value, true)}"`;
}

@ -5,6 +5,7 @@ export interface Element {
name: string;
attributes: Record<string, string | undefined>;
children: Node[];
is_html: boolean;
/** used for populating __svelte_meta */
start: number;
}

@ -5,10 +5,11 @@ import * as b from '#compiler/builders';
import {
build_assignment_value,
get_attribute_expression,
is_event_attribute
is_event_attribute,
is_expression_async
} from '../../../../utils/ast.js';
import { dev, locate_node } from '../../../../state.js';
import { should_proxy } from '../utils.js';
import { build_getter, should_proxy } from '../utils.js';
import { visit_assignment_expression } from '../../shared/assignments.js';
import { validate_mutation } from './shared/utils.js';
import { get_rune } from '../../../scope.js';
@ -36,14 +37,6 @@ function is_non_coercive_operator(operator) {
return ['=', '||=', '&&=', '??='].includes(operator);
}
/** @type {Record<string, string>} */
const callees = {
'=': '$.assign',
'&&=': '$.assign_and',
'||=': '$.assign_or',
'??=': '$.assign_nullish'
};
/**
* @param {AssignmentOperator} operator
* @param {Pattern} left
@ -147,7 +140,7 @@ function build_assignment(operator, left, right, context) {
// mutation
if (transform?.mutate) {
return transform.mutate(
let mutation = transform.mutate(
object,
b.assignment(
operator,
@ -155,12 +148,31 @@ function build_assignment(operator, left, right, context) {
/** @type {Expression} */ (context.visit(right))
)
);
if (binding.legacy_indirect_bindings.size > 0) {
mutation = b.sequence([
mutation,
b.call(
'$.invalidate_inner_signals',
b.arrow(
[],
b.block(
Array.from(binding.legacy_indirect_bindings).map((binding) =>
b.stmt(build_getter({ ...binding.node }, context.state))
)
)
)
)
]);
}
return mutation;
}
// in cases like `(object.items ??= []).push(value)`, we may need to warn
// if the value gets proxified, since the proxy _isn't_ the thing that
// will be pushed to. we do this by transforming it to something like
// `$.assign_nullish(object, 'items', [])`
// `$.assign(object, 'items', '??=', () => [])`
let should_transform =
dev &&
path.at(-1) !== 'ExpressionStatement' &&
@ -206,22 +218,23 @@ function build_assignment(operator, left, right, context) {
}
if (left.type === 'MemberExpression' && should_transform) {
const callee = callees[operator];
return /** @type {Expression} */ (
context.visit(
b.call(
callee,
/** @type {Expression} */ (left.object),
/** @type {Expression} */ (
left.computed
? left.property
: b.literal(/** @type {Identifier} */ (left.property).name)
),
right,
b.literal(locate_node(left))
)
)
const needs_lazy_getter = operator !== '=';
const needs_async = needs_lazy_getter && is_expression_async(right);
/** @type {Expression} */
let e = b.call(
needs_async ? '$.assign_async' : '$.assign',
/** @type {Expression} */ (left.object),
/** @type {Expression} */ (
left.computed ? left.property : b.literal(/** @type {Identifier} */ (left.property).name)
),
b.literal(operator),
needs_lazy_getter ? b.arrow([], right, needs_async) : right,
b.literal(locate_node(left))
);
if (needs_async) {
e = b.await(e);
}
return /** @type {Expression} */ (context.visit(e));
}
return null;

@ -71,14 +71,14 @@ export function AwaitBlock(node, context) {
'await'
);
if (node.metadata.expression.has_blockers()) {
if (node.metadata.expression.has_blockers() || node.metadata.expression.has_await) {
context.state.init.push(
b.stmt(
b.call(
'$.async',
context.state.node,
node.metadata.expression.blockers(),
b.array([]),
b.array([]), // {#await await ...} is special insofar that the await should not be waited on
b.arrow([context.state.node], b.block([stmt]))
)
)

@ -1,7 +1,6 @@
/** @import { Expression, Identifier, Pattern } from 'estree' */
/** @import { Expression, Identifier, Pattern, Statement } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */
/** @import { ExpressionMetadata } from '../../../nodes.js' */
import { dev } from '../../../../state.js';
import { extract_identifiers } from '../../../../utils/ast.js';
import * as b from '#compiler/builders';
@ -27,13 +26,7 @@ export function ConstTag(node, context) {
context.state.transform[declaration.id.name] = { read: get_value };
add_const_declaration(
context.state,
declaration.id,
expression,
node.metadata.expression,
context.state.scope.get_bindings(declaration)
);
add_const_declaration(context.state, declaration.id, expression, node.metadata);
} else {
const identifiers = extract_identifiers(declaration.id);
const tmp = b.id(context.state.scope.generate('computed_const'));
@ -70,13 +63,7 @@ export function ConstTag(node, context) {
expression = b.call('$.tag', expression, b.literal('[@const]'));
}
add_const_declaration(
context.state,
tmp,
expression,
node.metadata.expression,
context.state.scope.get_bindings(declaration)
);
add_const_declaration(context.state, tmp, expression, node.metadata);
for (const node of identifiers) {
context.state.transform[node.name] = {
@ -90,43 +77,34 @@ export function ConstTag(node, context) {
* @param {ComponentContext['state']} state
* @param {Identifier} id
* @param {Expression} expression
* @param {ExpressionMetadata} metadata
* @param {import('#compiler').Binding[]} bindings
* @param {AST.ConstTag['metadata']} metadata
*/
function add_const_declaration(state, id, expression, metadata, bindings) {
function add_const_declaration(state, id, expression, metadata) {
// we need to eagerly evaluate the expression in order to hit any
// 'Cannot access x before initialization' errors
const after = dev ? [b.stmt(b.call('$.get', id))] : [];
const has_await = metadata.has_await;
const blockers = [...metadata.dependencies]
const blockers = [...metadata.expression.dependencies]
.map((dep) => dep.blocker)
.filter((b) => b !== null && b.object !== state.async_consts?.id);
if (has_await || state.async_consts || blockers.length > 0) {
if (metadata.promises_id) {
const run = (state.async_consts ??= {
id: b.id(state.scope.generate('promises')),
id: metadata.promises_id,
thunks: []
});
state.consts.push(b.let(id));
const assignment = b.assignment('=', id, expression);
const body = after.length === 0 ? assignment : b.block([b.stmt(assignment), ...after]);
if (blockers.length === 1) {
run.thunks.push(b.thunk(b.member(/** @type {Expression} */ (blockers[0]), 'promise')));
} else if (blockers.length > 0) {
run.thunks.push(b.thunk(b.call('$.wait', b.array(blockers))));
}
run.thunks.push(b.thunk(body, has_await));
const blocker = b.member(run.id, b.literal(run.thunks.length - 1), true);
for (const binding of bindings) {
binding.blocker = blocker;
}
// keep the number of thunks pushed in sync with ConstTag in analysis phase
const assignment = b.assignment('=', id, expression);
run.thunks.push(b.thunk(assignment, metadata.expression.has_await));
} else {
state.consts.push(b.const(id, expression));
state.consts.push(...after);

@ -8,6 +8,10 @@ import * as b from '#compiler/builders';
* @param {ComponentContext} context
*/
export function DebugTag(node, context) {
const blockers = node.identifiers
.map((identifier) => context.state.scope.get(identifier.name)?.blocker)
.filter((blocker) => blocker != null);
const object = b.object(
node.identifiers.map((identifier) => {
const visited = b.call('$.snapshot', /** @type {Expression} */ (context.visit(identifier)));
@ -20,9 +24,11 @@ export function DebugTag(node, context) {
})
);
const call = b.call('console.log', object);
const args = [b.thunk(b.block([b.stmt(b.call('console.log', object)), b.debugger]))];
context.state.init.push(
b.stmt(b.call('$.template_effect', b.thunk(b.block([b.stmt(call), b.debugger]))))
);
if (blockers.length > 0) {
args.push(b.array([]), b.array([]), b.array(blockers));
}
context.state.init.push(b.stmt(b.call('$.template_effect', ...args)));
}

@ -63,6 +63,7 @@ export function Fragment(node, context) {
/** @type {ComponentClientTransformState} */
const state = {
...context.state,
is_standalone,
init: [],
snippets: [],
consts: [],
@ -128,7 +129,7 @@ export function Fragment(node, context) {
// no need to create a template, we can just use the existing block's anchor
process_children(trimmed, () => b.id('$$anchor'), false, {
...context,
state: { ...state, is_standalone }
state
});
} else {
/** @type {(is_text: boolean) => Expression} */

@ -9,7 +9,11 @@ import { build_expression } from './shared/utils.js';
* @param {ComponentContext} context
*/
export function HtmlTag(node, context) {
context.state.template.push_comment();
const is_controlled = node.metadata.is_controlled;
if (!is_controlled) {
context.state.template.push_comment();
}
const has_await = node.metadata.expression.has_await;
const has_blockers = node.metadata.expression.has_blockers();
@ -17,14 +21,17 @@ export function HtmlTag(node, context) {
const expression = build_expression(context, node.expression, node.metadata.expression);
const html = has_await ? b.call('$.get', b.id('$$html')) : expression;
const is_svg = context.state.metadata.namespace === 'svg';
const is_mathml = context.state.metadata.namespace === 'mathml';
// When is_controlled, the parent node already provides the correct namespace,
// so is_svg/is_mathml are only needed for the non-controlled path's wrapper element
const is_svg = !is_controlled && context.state.metadata.namespace === 'svg';
const is_mathml = !is_controlled && context.state.metadata.namespace === 'mathml';
const statement = b.stmt(
b.call(
'$.html',
context.state.node,
b.thunk(html),
is_controlled && b.true,
is_svg && b.true,
is_mathml && b.true,
is_ignored(node, 'hydration_html_changed') && b.true

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

Loading…
Cancel
Save