attempt merge

fix-15339
Rich Harris 5 hours ago
commit a11820f71d

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: properly separate multiline html blocks from each other in `print()`

@ -32,9 +32,9 @@ jobs:
os: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
- uses: actions/setup-node@v6
with:
node-version: ${{ matrix.node-version }}
cache: pnpm
@ -48,9 +48,9 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
- uses: actions/setup-node@v6
with:
node-version: 22
cache: pnpm
@ -65,9 +65,9 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
- uses: actions/setup-node@v6
with:
node-version: 24
cache: pnpm
@ -82,9 +82,9 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
- uses: actions/setup-node@v6
with:
node-version: 18
cache: pnpm
@ -103,9 +103,9 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
- uses: actions/setup-node@v6
with:
node-version: 18
cache: pnpm

@ -4,20 +4,20 @@ on:
issue_comment:
types: [created]
permissions: {}
jobs:
trigger:
runs-on: ubuntu-latest
if: github.repository == 'sveltejs/svelte' && github.event.issue.pull_request && startsWith(github.event.comment.body, '/ecosystem-ci run')
permissions:
issues: write # to add / delete reactions
issues: write # to add / delete reactions, post comments
pull-requests: write # to read PR data, and to add labels
actions: read # to check workflow status
contents: read # to clone the repo
steps:
- name: monitor action permissions
uses: GitHubSecurityLab/actions-permissions/monitor@v1
- name: check user authorization # user needs triage permission
uses: actions/github-script@v7
- name: Check User Permissions
uses: actions/github-script@v8
id: check-permissions
with:
script: |
@ -56,7 +56,7 @@ jobs:
}
- name: Get PR Data
uses: actions/github-script@v7
uses: actions/github-script@v8
id: get-pr-data
with:
script: |
@ -66,6 +66,37 @@ jobs:
repo: context.repo.repo,
pull_number: context.issue.number
})
const commentCreatedAt = new Date(context.payload.comment.created_at)
const commitPushedAt = new Date(pr.head.repo.pushed_at)
console.log(`Comment created at: ${commentCreatedAt.toISOString()}`)
console.log(`PR last pushed at: ${commitPushedAt.toISOString()}`)
// Check if any commits were pushed after the comment was created
if (commitPushedAt > commentCreatedAt) {
const errorMsg = [
'⚠️ Security warning: PR was updated after the trigger command was posted.',
'',
`Comment posted at: ${commentCreatedAt.toISOString()}`,
`PR last pushed at: ${commitPushedAt.toISOString()}`,
'',
'This could indicate an attempt to inject code after approval.',
'Please review the latest changes and re-run /ecosystem-ci run if they are acceptable.'
].join('\n')
core.setFailed(errorMsg)
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: errorMsg
})
throw new Error('PR was pushed to after comment was created')
}
return {
num: context.issue.number,
branchName: pr.head.ref,
@ -84,15 +115,16 @@ jobs:
svelte-ecosystem-ci
- name: Trigger Downstream Workflow
uses: actions/github-script@v7
uses: actions/github-script@v8
id: trigger
env:
COMMENT: ${{ github.event.comment.body }}
PR_DATA: ${{ steps.get-pr-data.outputs.result }}
with:
github-token: ${{ steps.generate-token.outputs.token }}
script: |
const comment = process.env.COMMENT.trim()
const prData = ${{ steps.get-pr-data.outputs.result }}
const prData = JSON.parse(process.env.PR_DATA)
const suite = comment.split('\n')[0].replace(/^\/ecosystem-ci run/, '').trim()

@ -14,9 +14,8 @@ jobs:
name: 'Update comment'
runs-on: ubuntu-latest
steps:
- uses: GitHubSecurityLab/actions-permissions/monitor@v1
- name: Download artifact
uses: actions/download-artifact@v4
uses: actions/download-artifact@v7
with:
name: output
github-token: ${{ secrets.GITHUB_TOKEN }}
@ -24,7 +23,7 @@ jobs:
- run: ls -R .
- name: 'Post or update comment'
uses: actions/github-script@v6
uses: actions/github-script@v8
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |

@ -1,6 +1,8 @@
name: Publish Any Commit
on: [push, pull_request]
permissions: {}
jobs:
build:
permissions: {}
@ -8,9 +10,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
- uses: actions/setup-node@v6
with:
node-version: 22.x
cache: pnpm
@ -23,7 +25,7 @@ jobs:
- run: pnpx pkg-pr-new publish --comment=off --json output.json --compact --no-template './packages/svelte'
- name: Add metadata to output
uses: actions/github-script@v6
uses: actions/github-script@v8
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
@ -34,7 +36,7 @@ jobs:
output.ref = context.ref;
fs.writeFileSync('output.json', JSON.stringify(output), 'utf8');
- name: Upload output
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
with:
name: output
path: ./output.json

@ -5,6 +5,11 @@ on:
branches:
- main
concurrency:
# prevent two release workflows from running at once
# race conditions here can result in releases failing
group: ${{ github.workflow }}
permissions: {}
jobs:
release:
@ -17,17 +22,16 @@ jobs:
name: Release
runs-on: ubuntu-latest
steps:
- uses: GitHubSecurityLab/actions-permissions/monitor@v1
- name: Checkout Repo
uses: actions/checkout@v4
uses: actions/checkout@v6
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
- name: Setup Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v6
with:
node-version: 18.x
node-version: 24.x
cache: pnpm
- name: Install
@ -45,4 +49,3 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_CONFIG_PROVENANCE: true
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

@ -15,6 +15,7 @@ packages/svelte/src/internal/client/warnings.js
packages/svelte/src/internal/shared/errors.js
packages/svelte/src/internal/shared/warnings.js
packages/svelte/src/internal/server/errors.js
packages/svelte/src/internal/server/warnings.js
packages/svelte/tests/migrate/samples/*/output.svelte
packages/svelte/tests/**/*.svelte
packages/svelte/tests/**/_expected*
@ -24,6 +25,10 @@ packages/svelte/tests/**/_output
packages/svelte/tests/**/shards/*.test.js
packages/svelte/tests/hydration/samples/*/_expected.html
packages/svelte/tests/hydration/samples/*/_override.html
packages/svelte/tests/parser-legacy/samples/*/_actual.json
packages/svelte/tests/parser-legacy/samples/*/output.json
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/src/*

@ -9,7 +9,7 @@ The [Open Source Guides](https://opensource.guide/) website has a collection of
## Get involved
There are many ways to contribute to Svelte, and many of them do not involve writing any code. Here's a few ideas to get started:
There are many ways to contribute to Svelte, and many of them do not involve writing any code. Here are a few ideas to get started:
- Simply start using Svelte. Go through the [Getting Started](https://svelte.dev/docs#getting-started) guide. Does everything work as expected? If not, we're always looking for improvements. Let us know by [opening an issue](#reporting-new-issues).
- Look through the [open issues](https://github.com/sveltejs/svelte/issues). A good starting point would be issues tagged [good first issue](https://github.com/sveltejs/svelte/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22). Provide workarounds, ask for clarification, or suggest labels. Help [triage issues](#triaging-issues-and-pull-requests).
@ -90,9 +90,9 @@ A good test plan has the exact commands you ran and their output, provides scree
#### Writing tests
All tests are located in `/test` folder.
All tests are located in the `/tests` folder.
Test samples are kept in `/test/xxx/samples` folder.
Test samples are kept in `/tests/xxx/samples` folders.
#### Running tests

@ -1,12 +1,5 @@
import { kairo_avoidable_owned, kairo_avoidable_unowned } from './kairo/kairo_avoidable.js';
import { kairo_broad_owned, kairo_broad_unowned } from './kairo/kairo_broad.js';
import { kairo_deep_owned, kairo_deep_unowned } from './kairo/kairo_deep.js';
import { kairo_diamond_owned, kairo_diamond_unowned } from './kairo/kairo_diamond.js';
import { kairo_mux_unowned, kairo_mux_owned } from './kairo/kairo_mux.js';
import { kairo_repeated_unowned, kairo_repeated_owned } from './kairo/kairo_repeated.js';
import { kairo_triangle_owned, kairo_triangle_unowned } from './kairo/kairo_triangle.js';
import { kairo_unstable_owned, kairo_unstable_unowned } from './kairo/kairo_unstable.js';
import { mol_bench_owned, mol_bench_unowned } from './mol_bench.js';
import fs from 'node:fs';
import path from 'node:path';
import {
sbench_create_0to1,
sbench_create_1000to1,
@ -19,10 +12,14 @@ import {
sbench_create_4to1,
sbench_create_signals
} from './sbench.js';
import { fileURLToPath } from 'node:url';
import { create_test } from './util.js';
// This benchmark has been adapted from the js-reactivity-benchmark (https://github.com/milomg/js-reactivity-benchmark)
// Not all tests are the same, and many parts have been tweaked to capture different data.
const dirname = path.dirname(fileURLToPath(import.meta.url));
export const reactivity_benchmarks = [
sbench_create_signals,
sbench_create_0to1,
@ -33,23 +30,16 @@ export const reactivity_benchmarks = [
sbench_create_1to2,
sbench_create_1to4,
sbench_create_1to8,
sbench_create_1to1000,
kairo_avoidable_owned,
kairo_avoidable_unowned,
kairo_broad_owned,
kairo_broad_unowned,
kairo_deep_owned,
kairo_deep_unowned,
kairo_diamond_owned,
kairo_diamond_unowned,
kairo_triangle_owned,
kairo_triangle_unowned,
kairo_mux_owned,
kairo_mux_unowned,
kairo_repeated_owned,
kairo_repeated_unowned,
kairo_unstable_owned,
kairo_unstable_unowned,
mol_bench_owned,
mol_bench_unowned
sbench_create_1to1000
];
for (const file of fs.readdirSync(`${dirname}/tests`)) {
if (!file.includes('.bench.')) continue;
const name = file.replace('.bench.js', '');
const module = await import(`${dirname}/tests/${file}`);
const { owned, unowned } = create_test(name, module.default);
reactivity_benchmarks.push(owned, unowned);
}

@ -1,91 +0,0 @@
import { assert, fastest_test } from '../../../utils.js';
import * as $ from 'svelte/internal/client';
import { busy } from './util.js';
function setup() {
let head = $.state(0);
let computed1 = $.derived(() => $.get(head));
let computed2 = $.derived(() => ($.get(computed1), 0));
let computed3 = $.derived(() => (busy(), $.get(computed2) + 1)); // heavy computation
let computed4 = $.derived(() => $.get(computed3) + 2);
let computed5 = $.derived(() => $.get(computed4) + 3);
const destroy = $.effect_root(() => {
$.render_effect(() => {
$.get(computed5);
busy(); // heavy side effect
});
});
return {
destroy,
run() {
$.flush(() => {
$.set(head, 1);
});
assert($.get(computed5) === 6);
for (let i = 0; i < 1000; i++) {
$.flush(() => {
$.set(head, i);
});
assert($.get(computed5) === 6);
}
}
};
}
export async function kairo_avoidable_unowned() {
// Do 10 loops to warm up JIT
for (let i = 0; i < 10; i++) {
const { run, destroy } = setup();
run();
destroy();
}
const { run, destroy } = setup();
const { timing } = await fastest_test(10, () => {
for (let i = 0; i < 1000; i++) {
run();
}
});
destroy();
return {
benchmark: 'kairo_avoidable_unowned',
time: timing.time.toFixed(2),
gc_time: timing.gc_time.toFixed(2)
};
}
export async function kairo_avoidable_owned() {
let run, destroy;
const destroy_owned = $.effect_root(() => {
// Do 10 loops to warm up JIT
for (let i = 0; i < 10; i++) {
const { run, destroy } = setup();
run();
destroy();
}
({ run, destroy } = setup());
});
const { timing } = await fastest_test(10, () => {
for (let i = 0; i < 1000; i++) {
run();
}
});
// @ts-ignore
destroy();
destroy_owned();
return {
benchmark: 'kairo_avoidable_owned',
time: timing.time.toFixed(2),
gc_time: timing.gc_time.toFixed(2)
};
}

@ -1,97 +0,0 @@
import { assert, fastest_test } from '../../../utils.js';
import * as $ from 'svelte/internal/client';
function setup() {
let head = $.state(0);
let last = head;
let counter = 0;
const destroy = $.effect_root(() => {
for (let i = 0; i < 50; i++) {
let current = $.derived(() => {
return $.get(head) + i;
});
let current2 = $.derived(() => {
return $.get(current) + 1;
});
$.render_effect(() => {
$.get(current2);
counter++;
});
last = current2;
}
});
return {
destroy,
run() {
$.flush(() => {
$.set(head, 1);
});
counter = 0;
for (let i = 0; i < 50; i++) {
$.flush(() => {
$.set(head, i);
});
assert($.get(last) === i + 50);
}
assert(counter === 50 * 50);
}
};
}
export async function kairo_broad_unowned() {
// Do 10 loops to warm up JIT
for (let i = 0; i < 10; i++) {
const { run, destroy } = setup();
run();
destroy();
}
const { run, destroy } = setup();
const { timing } = await fastest_test(10, () => {
for (let i = 0; i < 1000; i++) {
run();
}
});
destroy();
return {
benchmark: 'kairo_broad_unowned',
time: timing.time.toFixed(2),
gc_time: timing.gc_time.toFixed(2)
};
}
export async function kairo_broad_owned() {
let run, destroy;
const destroy_owned = $.effect_root(() => {
// Do 10 loops to warm up JIT
for (let i = 0; i < 10; i++) {
const { run, destroy } = setup();
run();
destroy();
}
({ run, destroy } = setup());
});
const { timing } = await fastest_test(10, () => {
for (let i = 0; i < 1000; i++) {
run();
}
});
// @ts-ignore
destroy();
destroy_owned();
return {
benchmark: 'kairo_broad_owned',
time: timing.time.toFixed(2),
gc_time: timing.gc_time.toFixed(2)
};
}

@ -1,97 +0,0 @@
import { assert, fastest_test } from '../../../utils.js';
import * as $ from 'svelte/internal/client';
let len = 50;
const iter = 50;
function setup() {
let head = $.state(0);
let current = head;
for (let i = 0; i < len; i++) {
let c = current;
current = $.derived(() => {
return $.get(c) + 1;
});
}
let counter = 0;
const destroy = $.effect_root(() => {
$.render_effect(() => {
$.get(current);
counter++;
});
});
return {
destroy,
run() {
$.flush(() => {
$.set(head, 1);
});
counter = 0;
for (let i = 0; i < iter; i++) {
$.flush(() => {
$.set(head, i);
});
assert($.get(current) === len + i);
}
assert(counter === iter);
}
};
}
export async function kairo_deep_unowned() {
// Do 10 loops to warm up JIT
for (let i = 0; i < 10; i++) {
const { run, destroy } = setup();
run();
destroy();
}
const { run, destroy } = setup();
const { timing } = await fastest_test(10, () => {
for (let i = 0; i < 1000; i++) {
run();
}
});
destroy();
return {
benchmark: 'kairo_deep_unowned',
time: timing.time.toFixed(2),
gc_time: timing.gc_time.toFixed(2)
};
}
export async function kairo_deep_owned() {
let run, destroy;
const destroy_owned = $.effect_root(() => {
// Do 10 loops to warm up JIT
for (let i = 0; i < 10; i++) {
const { run, destroy } = setup();
run();
destroy();
}
({ run, destroy } = setup());
});
const { timing } = await fastest_test(10, () => {
for (let i = 0; i < 1000; i++) {
run();
}
});
// @ts-ignore
destroy();
destroy_owned();
return {
benchmark: 'kairo_deep_owned',
time: timing.time.toFixed(2),
gc_time: timing.gc_time.toFixed(2)
};
}

@ -1,101 +0,0 @@
import { assert, fastest_test } from '../../../utils.js';
import * as $ from 'svelte/internal/client';
let width = 5;
function setup() {
let head = $.state(0);
let current = [];
for (let i = 0; i < width; i++) {
current.push(
$.derived(() => {
return $.get(head) + 1;
})
);
}
let sum = $.derived(() => {
return current.map((x) => $.get(x)).reduce((a, b) => a + b, 0);
});
let counter = 0;
const destroy = $.effect_root(() => {
$.render_effect(() => {
$.get(sum);
counter++;
});
});
return {
destroy,
run() {
$.flush(() => {
$.set(head, 1);
});
assert($.get(sum) === 2 * width);
counter = 0;
for (let i = 0; i < 500; i++) {
$.flush(() => {
$.set(head, i);
});
assert($.get(sum) === (i + 1) * width);
}
assert(counter === 500);
}
};
}
export async function kairo_diamond_unowned() {
// Do 10 loops to warm up JIT
for (let i = 0; i < 10; i++) {
const { run, destroy } = setup();
run();
destroy();
}
const { run, destroy } = setup();
const { timing } = await fastest_test(10, () => {
for (let i = 0; i < 1000; i++) {
run();
}
});
destroy();
return {
benchmark: 'kairo_diamond_unowned',
time: timing.time.toFixed(2),
gc_time: timing.gc_time.toFixed(2)
};
}
export async function kairo_diamond_owned() {
let run, destroy;
const destroy_owned = $.effect_root(() => {
// Do 10 loops to warm up JIT
for (let i = 0; i < 10; i++) {
const { run, destroy } = setup();
run();
destroy();
}
({ run, destroy } = setup());
});
const { timing } = await fastest_test(10, () => {
for (let i = 0; i < 1000; i++) {
run();
}
});
// @ts-ignore
destroy();
destroy_owned();
return {
benchmark: 'kairo_diamond_owned',
time: timing.time.toFixed(2),
gc_time: timing.gc_time.toFixed(2)
};
}

@ -1,94 +0,0 @@
import { assert, fastest_test } from '../../../utils.js';
import * as $ from 'svelte/internal/client';
function setup() {
let heads = new Array(100).fill(null).map((_) => $.state(0));
const mux = $.derived(() => {
return Object.fromEntries(heads.map((h) => $.get(h)).entries());
});
const splited = heads
.map((_, index) => $.derived(() => $.get(mux)[index]))
.map((x) => $.derived(() => $.get(x) + 1));
const destroy = $.effect_root(() => {
splited.forEach((x) => {
$.render_effect(() => {
$.get(x);
});
});
});
return {
destroy,
run() {
for (let i = 0; i < 10; i++) {
$.flush(() => {
$.set(heads[i], i);
});
assert($.get(splited[i]) === i + 1);
}
for (let i = 0; i < 10; i++) {
$.flush(() => {
$.set(heads[i], i * 2);
});
assert($.get(splited[i]) === i * 2 + 1);
}
}
};
}
export async function kairo_mux_unowned() {
// Do 10 loops to warm up JIT
for (let i = 0; i < 10; i++) {
const { run, destroy } = setup();
run();
destroy();
}
const { run, destroy } = setup();
const { timing } = await fastest_test(10, () => {
for (let i = 0; i < 1000; i++) {
run();
}
});
destroy();
return {
benchmark: 'kairo_mux_unowned',
time: timing.time.toFixed(2),
gc_time: timing.gc_time.toFixed(2)
};
}
export async function kairo_mux_owned() {
let run, destroy;
const destroy_owned = $.effect_root(() => {
// Do 10 loops to warm up JIT
for (let i = 0; i < 10; i++) {
const { run, destroy } = setup();
run();
destroy();
}
({ run, destroy } = setup());
});
const { timing } = await fastest_test(10, () => {
for (let i = 0; i < 1000; i++) {
run();
}
});
// @ts-ignore
destroy();
destroy_owned();
return {
benchmark: 'kairo_mux_owned',
time: timing.time.toFixed(2),
gc_time: timing.gc_time.toFixed(2)
};
}

@ -1,98 +0,0 @@
import { assert, fastest_test } from '../../../utils.js';
import * as $ from 'svelte/internal/client';
let size = 30;
function setup() {
let head = $.state(0);
let current = $.derived(() => {
let result = 0;
for (let i = 0; i < size; i++) {
result += $.get(head);
}
return result;
});
let counter = 0;
const destroy = $.effect_root(() => {
$.render_effect(() => {
$.get(current);
counter++;
});
});
return {
destroy,
run() {
$.flush(() => {
$.set(head, 1);
});
assert($.get(current) === size);
counter = 0;
for (let i = 0; i < 100; i++) {
$.flush(() => {
$.set(head, i);
});
assert($.get(current) === i * size);
}
assert(counter === 100);
}
};
}
export async function kairo_repeated_unowned() {
// Do 10 loops to warm up JIT
for (let i = 0; i < 10; i++) {
const { run, destroy } = setup();
run();
destroy();
}
const { run, destroy } = setup();
const { timing } = await fastest_test(10, () => {
for (let i = 0; i < 1000; i++) {
run();
}
});
destroy();
return {
benchmark: 'kairo_repeated_unowned',
time: timing.time.toFixed(2),
gc_time: timing.gc_time.toFixed(2)
};
}
export async function kairo_repeated_owned() {
let run, destroy;
const destroy_owned = $.effect_root(() => {
// Do 10 loops to warm up JIT
for (let i = 0; i < 10; i++) {
const { run, destroy } = setup();
run();
destroy();
}
({ run, destroy } = setup());
});
const { timing } = await fastest_test(10, () => {
for (let i = 0; i < 1000; i++) {
run();
}
});
// @ts-ignore
destroy();
destroy_owned();
return {
benchmark: 'kairo_repeated_owned',
time: timing.time.toFixed(2),
gc_time: timing.gc_time.toFixed(2)
};
}

@ -1,111 +0,0 @@
import { assert, fastest_test } from '../../../utils.js';
import * as $ from 'svelte/internal/client';
let width = 10;
function count(number) {
return new Array(number)
.fill(0)
.map((_, i) => i + 1)
.reduce((x, y) => x + y, 0);
}
function setup() {
let head = $.state(0);
let current = head;
let list = [];
for (let i = 0; i < width; i++) {
let c = current;
list.push(current);
current = $.derived(() => {
return $.get(c) + 1;
});
}
let sum = $.derived(() => {
return list.map((x) => $.get(x)).reduce((a, b) => a + b, 0);
});
let counter = 0;
const destroy = $.effect_root(() => {
$.render_effect(() => {
$.get(sum);
counter++;
});
});
return {
destroy,
run() {
const constant = count(width);
$.flush(() => {
$.set(head, 1);
});
assert($.get(sum) === constant);
counter = 0;
for (let i = 0; i < 100; i++) {
$.flush(() => {
$.set(head, i);
});
assert($.get(sum) === constant - width + i * width);
}
assert(counter === 100);
}
};
}
export async function kairo_triangle_unowned() {
// Do 10 loops to warm up JIT
for (let i = 0; i < 10; i++) {
const { run, destroy } = setup();
run();
destroy();
}
const { run, destroy } = setup();
const { timing } = await fastest_test(10, () => {
for (let i = 0; i < 1000; i++) {
run();
}
});
destroy();
return {
benchmark: 'kairo_triangle_unowned',
time: timing.time.toFixed(2),
gc_time: timing.gc_time.toFixed(2)
};
}
export async function kairo_triangle_owned() {
let run, destroy;
const destroy_owned = $.effect_root(() => {
// Do 10 loops to warm up JIT
for (let i = 0; i < 10; i++) {
const { run, destroy } = setup();
run();
destroy();
}
({ run, destroy } = setup());
});
const { timing } = await fastest_test(10, () => {
for (let i = 0; i < 1000; i++) {
run();
}
});
// @ts-ignore
destroy();
destroy_owned();
return {
benchmark: 'kairo_triangle_owned',
time: timing.time.toFixed(2),
gc_time: timing.gc_time.toFixed(2)
};
}

@ -1,97 +0,0 @@
import { assert, fastest_test } from '../../../utils.js';
import * as $ from 'svelte/internal/client';
function setup() {
let head = $.state(0);
const double = $.derived(() => $.get(head) * 2);
const inverse = $.derived(() => -$.get(head));
let current = $.derived(() => {
let result = 0;
for (let i = 0; i < 20; i++) {
result += $.get(head) % 2 ? $.get(double) : $.get(inverse);
}
return result;
});
let counter = 0;
const destroy = $.effect_root(() => {
$.render_effect(() => {
$.get(current);
counter++;
});
});
return {
destroy,
run() {
$.flush(() => {
$.set(head, 1);
});
assert($.get(current) === 40);
counter = 0;
for (let i = 0; i < 100; i++) {
$.flush(() => {
$.set(head, i);
});
}
assert(counter === 100);
}
};
}
export async function kairo_unstable_unowned() {
// Do 10 loops to warm up JIT
for (let i = 0; i < 10; i++) {
const { run, destroy } = setup();
run();
destroy();
}
const { run, destroy } = setup();
const { timing } = await fastest_test(10, () => {
for (let i = 0; i < 1000; i++) {
run();
}
});
destroy();
return {
benchmark: 'kairo_unstable_unowned',
time: timing.time.toFixed(2),
gc_time: timing.gc_time.toFixed(2)
};
}
export async function kairo_unstable_owned() {
let run, destroy;
const destroy_owned = $.effect_root(() => {
// Do 10 loops to warm up JIT
for (let i = 0; i < 10; i++) {
const { run, destroy } = setup();
run();
destroy();
}
({ run, destroy } = setup());
});
const { timing } = await fastest_test(10, () => {
for (let i = 0; i < 1000; i++) {
run();
}
});
// @ts-ignore
destroy();
destroy_owned();
return {
benchmark: 'kairo_unstable_owned',
time: timing.time.toFixed(2),
gc_time: timing.gc_time.toFixed(2)
};
}

@ -1,6 +0,0 @@
export function busy() {
let a = 0;
for (let i = 0; i < 1_00; i++) {
a++;
}
}

@ -1,3 +1,4 @@
/** @import { Source } from '../../../packages/svelte/src/internal/client/types.js' */
import { fastest_test } from '../../utils.js';
import * as $ from '../../../packages/svelte/src/internal/client/index.js';
@ -7,360 +8,177 @@ const COUNT = 1e5;
* @param {number} n
* @param {any[]} sources
*/
function create_data_signals(n, sources) {
function create_sources(n, sources) {
for (let i = 0; i < n; i++) {
sources[i] = $.state(i);
}
return sources;
}
/**
* @param {number} i
* @param {Source<number>} source
*/
function create_computation_0(i) {
$.derived(() => i);
function create_derived(source) {
$.derived(() => $.get(source));
}
/**
* @param {any} s1
*/
function create_computation_1(s1) {
$.derived(() => $.get(s1));
}
/**
* @param {any} s1
* @param {any} s2
*
* @param {string} label
* @param {(n: number, sources: Array<Source<number>>) => void} fn
* @param {number} count
* @param {number} num_sources
*/
function create_computation_2(s1, s2) {
$.derived(() => $.get(s1) + $.get(s2));
}
function create_sbench_test(label, count, num_sources, fn) {
return {
label,
fn: async () => {
// Do 3 loops to warm up JIT
for (let i = 0; i < 3; i++) {
fn(count, create_sources(num_sources, []));
}
function create_computation_1000(ss, offset) {
$.derived(() => {
let sum = 0;
for (let i = 0; i < 1000; i++) {
sum += $.get(ss[offset + i]);
return await fastest_test(10, () => {
const destroy = $.effect_root(() => {
for (let i = 0; i < 10; i++) {
fn(count, create_sources(num_sources, []));
}
});
destroy();
});
}
return sum;
});
};
}
/**
* @param {number} n
*/
function create_computations_0to1(n) {
for (let i = 0; i < n; i++) {
create_computation_0(i);
}
}
export const sbench_create_signals = create_sbench_test(
'sbench_create_signals',
COUNT,
COUNT,
create_sources
);
/**
* @param {number} n
* @param {any[]} sources
*/
function create_computations_1to1(n, sources) {
for (let i = 0; i < n; i++) {
const source = sources[i];
create_computation_1(source);
}
}
/**
* @param {number} n
* @param {any[]} sources
*/
function create_computations_2to1(n, sources) {
export const sbench_create_0to1 = create_sbench_test('sbench_create_0to1', COUNT, 0, (n) => {
for (let i = 0; i < n; i++) {
create_computation_2(sources[i * 2], sources[i * 2 + 1]);
$.derived(() => i);
}
}
function create_computation_4(s1, s2, s3, s4) {
$.derived(() => $.get(s1) + $.get(s2) + $.get(s3) + $.get(s4));
}
});
function create_computations_1000to1(n, sources) {
for (let i = 0; i < n; i++) {
create_computation_1000(sources, i * 1000);
}
}
function create_computations_1to2(n, sources) {
for (let i = 0; i < n / 2; i++) {
const source = sources[i];
create_computation_1(source);
create_computation_1(source);
}
}
function create_computations_1to4(n, sources) {
for (let i = 0; i < n / 4; i++) {
const source = sources[i];
create_computation_1(source);
create_computation_1(source);
create_computation_1(source);
create_computation_1(source);
}
}
function create_computations_1to8(n, sources) {
for (let i = 0; i < n / 8; i++) {
const source = sources[i];
create_computation_1(source);
create_computation_1(source);
create_computation_1(source);
create_computation_1(source);
create_computation_1(source);
create_computation_1(source);
create_computation_1(source);
create_computation_1(source);
}
}
function create_computations_1to1000(n, sources) {
for (let i = 0; i < n / 1000; i++) {
const source = sources[i];
for (let j = 0; j < 1000; j++) {
create_computation_1(source);
export const sbench_create_1to1 = create_sbench_test(
'sbench_create_1to1',
COUNT,
COUNT,
(n, sources) => {
for (let i = 0; i < n; i++) {
create_derived(sources[i]);
}
}
}
function create_computations_4to1(n, sources) {
for (let i = 0; i < n; i++) {
create_computation_4(
sources[i * 4],
sources[i * 4 + 1],
sources[i * 4 + 2],
sources[i * 4 + 3]
);
}
}
/**
* @param {any} fn
* @param {number} count
* @param {number} scount
*/
function bench(fn, count, scount) {
let sources = create_data_signals(scount, []);
fn(count, sources);
}
export async function sbench_create_signals() {
// Do 3 loops to warm up JIT
for (let i = 0; i < 3; i++) {
bench(create_data_signals, COUNT, COUNT);
}
);
const { timing } = await fastest_test(10, () => {
for (let i = 0; i < 100; i++) {
bench(create_data_signals, COUNT, COUNT);
export const sbench_create_2to1 = create_sbench_test(
'sbench_create_2to1',
COUNT / 2,
COUNT,
(n, sources) => {
for (let i = 0; i < n; i++) {
$.derived(() => $.get(sources[i * 2]) + $.get(sources[i * 2 + 1]));
}
});
return {
benchmark: 'sbench_create_signals',
time: timing.time.toFixed(2),
gc_time: timing.gc_time.toFixed(2)
};
}
export async function sbench_create_0to1() {
// Do 3 loops to warm up JIT
for (let i = 0; i < 3; i++) {
bench(create_computations_0to1, COUNT, 0);
}
const { timing } = await fastest_test(10, () => {
const destroy = $.effect_root(() => {
for (let i = 0; i < 10; i++) {
bench(create_computations_0to1, COUNT, 0);
}
});
destroy();
});
return {
benchmark: 'sbench_create_0to1',
time: timing.time.toFixed(2),
gc_time: timing.gc_time.toFixed(2)
};
}
export async function sbench_create_1to1() {
// Do 3 loops to warm up JIT
for (let i = 0; i < 3; i++) {
bench(create_computations_1to1, COUNT, COUNT);
}
const { timing } = await fastest_test(10, () => {
const destroy = $.effect_root(() => {
for (let i = 0; i < 10; i++) {
bench(create_computations_1to1, COUNT, COUNT);
}
});
destroy();
});
return {
benchmark: 'sbench_create_1to1',
time: timing.time.toFixed(2),
gc_time: timing.gc_time.toFixed(2)
};
}
export async function sbench_create_2to1() {
// Do 3 loops to warm up JIT
for (let i = 0; i < 3; i++) {
bench(create_computations_2to1, COUNT / 2, COUNT);
}
const { timing } = await fastest_test(10, () => {
const destroy = $.effect_root(() => {
for (let i = 0; i < 10; i++) {
bench(create_computations_2to1, COUNT / 2, COUNT);
}
});
destroy();
});
return {
benchmark: 'sbench_create_2to1',
time: timing.time.toFixed(2),
gc_time: timing.gc_time.toFixed(2)
};
}
export async function sbench_create_4to1() {
// Do 3 loops to warm up JIT
for (let i = 0; i < 3; i++) {
bench(create_computations_4to1, COUNT / 4, COUNT);
);
export const sbench_create_4to1 = create_sbench_test(
'sbench_create_4to1',
COUNT / 4,
COUNT,
(n, sources) => {
for (let i = 0; i < n; i++) {
$.derived(
() =>
$.get(sources[i * 4]) +
$.get(sources[i * 4 + 1]) +
$.get(sources[i * 4 + 2]) +
$.get(sources[i * 4 + 3])
);
}
}
const { timing } = await fastest_test(10, () => {
const destroy = $.effect_root(() => {
for (let i = 0; i < 10; i++) {
bench(create_computations_4to1, COUNT / 4, COUNT);
}
});
destroy();
});
return {
benchmark: 'sbench_create_4to1',
time: timing.time.toFixed(2),
gc_time: timing.gc_time.toFixed(2)
};
}
export async function sbench_create_1000to1() {
// Do 3 loops to warm up JIT
for (let i = 0; i < 3; i++) {
bench(create_computations_1000to1, COUNT / 1000, COUNT);
);
export const sbench_create_1000to1 = create_sbench_test(
'sbench_create_1000to1',
COUNT / 1000,
COUNT,
(n, sources) => {
for (let i = 0; i < n; i++) {
const offset = i * 1000;
$.derived(() => {
let sum = 0;
for (let i = 0; i < 1000; i++) {
sum += $.get(sources[offset + i]);
}
return sum;
});
}
}
const { timing } = await fastest_test(10, () => {
const destroy = $.effect_root(() => {
for (let i = 0; i < 10; i++) {
bench(create_computations_1000to1, COUNT / 1000, COUNT);
}
});
destroy();
});
return {
benchmark: 'sbench_create_1000to1',
time: timing.time.toFixed(2),
gc_time: timing.gc_time.toFixed(2)
};
}
export async function sbench_create_1to2() {
// Do 3 loops to warm up JIT
for (let i = 0; i < 3; i++) {
bench(create_computations_1to2, COUNT, COUNT / 2);
);
export const sbench_create_1to2 = create_sbench_test(
'sbench_create_1to2',
COUNT,
COUNT / 2,
(n, sources) => {
for (let i = 0; i < n / 2; i++) {
const source = sources[i];
create_derived(source);
create_derived(source);
}
}
const { timing } = await fastest_test(10, () => {
const destroy = $.effect_root(() => {
for (let i = 0; i < 10; i++) {
bench(create_computations_1to2, COUNT, COUNT / 2);
}
});
destroy();
});
return {
benchmark: 'sbench_create_1to2',
time: timing.time.toFixed(2),
gc_time: timing.gc_time.toFixed(2)
};
}
export async function sbench_create_1to4() {
// Do 3 loops to warm up JIT
for (let i = 0; i < 3; i++) {
bench(create_computations_1to4, COUNT, COUNT / 4);
);
export const sbench_create_1to4 = create_sbench_test(
'sbench_create_1to4',
COUNT,
COUNT / 4,
(n, sources) => {
for (let i = 0; i < n / 4; i++) {
const source = sources[i];
create_derived(source);
create_derived(source);
create_derived(source);
create_derived(source);
}
}
const { timing } = await fastest_test(10, () => {
const destroy = $.effect_root(() => {
for (let i = 0; i < 10; i++) {
bench(create_computations_1to4, COUNT, COUNT / 4);
}
});
destroy();
});
return {
benchmark: 'sbench_create_1to4',
time: timing.time.toFixed(2),
gc_time: timing.gc_time.toFixed(2)
};
}
export async function sbench_create_1to8() {
// Do 3 loops to warm up JIT
for (let i = 0; i < 3; i++) {
bench(create_computations_1to8, COUNT, COUNT / 8);
);
export const sbench_create_1to8 = create_sbench_test(
'sbench_create_1to8',
COUNT,
COUNT / 8,
(n, sources) => {
for (let i = 0; i < n / 8; i++) {
const source = sources[i];
create_derived(source);
create_derived(source);
create_derived(source);
create_derived(source);
create_derived(source);
create_derived(source);
create_derived(source);
create_derived(source);
}
}
const { timing } = await fastest_test(10, () => {
const destroy = $.effect_root(() => {
for (let i = 0; i < 10; i++) {
bench(create_computations_1to8, COUNT, COUNT / 8);
);
export const sbench_create_1to1000 = create_sbench_test(
'sbench_create_1to1000',
COUNT,
COUNT / 1000,
(n, sources) => {
for (let i = 0; i < n / 1000; i++) {
const source = sources[i];
for (let j = 0; j < 1000; j++) {
create_derived(source);
}
});
destroy();
});
return {
benchmark: 'sbench_create_1to8',
time: timing.time.toFixed(2),
gc_time: timing.gc_time.toFixed(2)
};
}
export async function sbench_create_1to1000() {
// Do 3 loops to warm up JIT
for (let i = 0; i < 3; i++) {
bench(create_computations_1to1000, COUNT, COUNT / 1000);
}
}
const { timing } = await fastest_test(10, () => {
const destroy = $.effect_root(() => {
for (let i = 0; i < 10; i++) {
bench(create_computations_1to1000, COUNT, COUNT / 1000);
}
});
destroy();
});
return {
benchmark: 'sbench_create_1to1000',
time: timing.time.toFixed(2),
gc_time: timing.gc_time.toFixed(2)
};
}
);

@ -0,0 +1,35 @@
import assert from 'node:assert';
import * as $ from 'svelte/internal/client';
import { busy } from '../util.js';
export default () => {
let head = $.state(0);
let computed1 = $.derived(() => $.get(head));
let computed2 = $.derived(() => ($.get(computed1), 0));
let computed3 = $.derived(() => (busy(), $.get(computed2) + 1)); // heavy computation
let computed4 = $.derived(() => $.get(computed3) + 2);
let computed5 = $.derived(() => $.get(computed4) + 3);
const destroy = $.effect_root(() => {
$.render_effect(() => {
$.get(computed5);
busy(); // heavy side effect
});
});
return {
destroy,
run() {
$.flush(() => {
$.set(head, 1);
});
assert.equal($.get(computed5), 6);
for (let i = 0; i < 1000; i++) {
$.flush(() => {
$.set(head, i);
});
assert.equal($.get(computed5), 6);
}
}
};
};

@ -0,0 +1,41 @@
import assert from 'node:assert';
import * as $ from 'svelte/internal/client';
export default () => {
let head = $.state(0);
let last = head;
let counter = 0;
const destroy = $.effect_root(() => {
for (let i = 0; i < 50; i++) {
let current = $.derived(() => {
return $.get(head) + i;
});
let current2 = $.derived(() => {
return $.get(current) + 1;
});
$.render_effect(() => {
$.get(current2);
counter++;
});
last = current2;
}
});
return {
destroy,
run() {
$.flush(() => {
$.set(head, 1);
});
counter = 0;
for (let i = 0; i < 50; i++) {
$.flush(() => {
$.set(head, i);
});
assert.equal($.get(last), i + 50);
}
assert.equal(counter, 50 * 50);
}
};
};

@ -0,0 +1,41 @@
import assert from 'node:assert';
import * as $ from 'svelte/internal/client';
let len = 50;
const iter = 50;
export default () => {
let head = $.state(0);
let current = head;
for (let i = 0; i < len; i++) {
let c = current;
current = $.derived(() => {
return $.get(c) + 1;
});
}
let counter = 0;
const destroy = $.effect_root(() => {
$.render_effect(() => {
$.get(current);
counter++;
});
});
return {
destroy,
run() {
$.flush(() => {
$.set(head, 1);
});
counter = 0;
for (let i = 0; i < iter; i++) {
$.flush(() => {
$.set(head, i);
});
assert.equal($.get(current), len + i);
}
assert.equal(counter, iter);
}
};
};

@ -0,0 +1,45 @@
import assert from 'node:assert';
import * as $ from 'svelte/internal/client';
let width = 5;
export default () => {
let head = $.state(0);
let current = [];
for (let i = 0; i < width; i++) {
current.push(
$.derived(() => {
return $.get(head) + 1;
})
);
}
let sum = $.derived(() => {
return current.map((x) => $.get(x)).reduce((a, b) => a + b, 0);
});
let counter = 0;
const destroy = $.effect_root(() => {
$.render_effect(() => {
$.get(sum);
counter++;
});
});
return {
destroy,
run() {
$.flush(() => {
$.set(head, 1);
});
assert.equal($.get(sum), 2 * width);
counter = 0;
for (let i = 0; i < 500; i++) {
$.flush(() => {
$.set(head, i);
});
assert.equal($.get(sum), (i + 1) * width);
}
assert.equal(counter, 500);
}
};
};

@ -0,0 +1,38 @@
import assert from 'node:assert';
import * as $ from 'svelte/internal/client';
export default () => {
let heads = new Array(100).fill(null).map((_) => $.state(0));
const mux = $.derived(() => {
return Object.fromEntries(heads.map((h) => $.get(h)).entries());
});
const splited = heads
.map((_, index) => $.derived(() => $.get(mux)[index]))
.map((x) => $.derived(() => $.get(x) + 1));
const destroy = $.effect_root(() => {
splited.forEach((x) => {
$.render_effect(() => {
$.get(x);
});
});
});
return {
destroy,
run() {
for (let i = 0; i < 10; i++) {
$.flush(() => {
$.set(heads[i], i);
});
assert.equal($.get(splited[i]), i + 1);
}
for (let i = 0; i < 10; i++) {
$.flush(() => {
$.set(heads[i], i * 2);
});
assert.equal($.get(splited[i]), i * 2 + 1);
}
}
};
};

@ -0,0 +1,42 @@
import assert from 'node:assert';
import * as $ from 'svelte/internal/client';
let size = 30;
export default () => {
let head = $.state(0);
let current = $.derived(() => {
let result = 0;
for (let i = 0; i < size; i++) {
result += $.get(head);
}
return result;
});
let counter = 0;
const destroy = $.effect_root(() => {
$.render_effect(() => {
$.get(current);
counter++;
});
});
return {
destroy,
run() {
$.flush(() => {
$.set(head, 1);
});
assert.equal($.get(current), size);
counter = 0;
for (let i = 0; i < 100; i++) {
$.flush(() => {
$.set(head, i);
});
assert.equal($.get(current), i * size);
}
assert.equal(counter, 100);
}
};
};

@ -0,0 +1,55 @@
import assert from 'node:assert';
import * as $ from 'svelte/internal/client';
let width = 10;
function count(number) {
return new Array(number)
.fill(0)
.map((_, i) => i + 1)
.reduce((x, y) => x + y, 0);
}
export default () => {
let head = $.state(0);
let current = head;
let list = [];
for (let i = 0; i < width; i++) {
let c = current;
list.push(current);
current = $.derived(() => {
return $.get(c) + 1;
});
}
let sum = $.derived(() => {
return list.map((x) => $.get(x)).reduce((a, b) => a + b, 0);
});
let counter = 0;
const destroy = $.effect_root(() => {
$.render_effect(() => {
$.get(sum);
counter++;
});
});
return {
destroy,
run() {
const constant = count(width);
$.flush(() => {
$.set(head, 1);
});
assert.equal($.get(sum), constant);
counter = 0;
for (let i = 0; i < 100; i++) {
$.flush(() => {
$.set(head, i);
});
assert.equal($.get(sum), constant - width + i * width);
}
assert.equal(counter, 100);
}
};
};

@ -0,0 +1,41 @@
import assert from 'node:assert';
import * as $ from 'svelte/internal/client';
export default () => {
let head = $.state(0);
const double = $.derived(() => $.get(head) * 2);
const inverse = $.derived(() => -$.get(head));
let current = $.derived(() => {
let result = 0;
for (let i = 0; i < 20; i++) {
result += $.get(head) % 2 ? $.get(double) : $.get(inverse);
}
return result;
});
let counter = 0;
const destroy = $.effect_root(() => {
$.render_effect(() => {
$.get(current);
counter++;
});
});
return {
destroy,
run() {
$.flush(() => {
$.set(head, 1);
});
assert.equal($.get(current), 40);
counter = 0;
for (let i = 0; i < 100; i++) {
$.flush(() => {
$.set(head, i);
});
}
assert.equal(counter, 100);
}
};
};

@ -1,4 +1,4 @@
import { assert, fastest_test } from '../../utils.js';
import assert from 'node:assert';
import * as $ from 'svelte/internal/client';
/**
@ -18,7 +18,7 @@ function hard(n) {
const numbers = Array.from({ length: 5 }, (_, i) => i);
function setup() {
export default () => {
let res = [];
const A = $.state(0);
const B = $.state(0);
@ -59,63 +59,10 @@ function setup() {
$.set(A, 2 + i * 2);
$.set(B, 2);
});
assert(res[0] === 3198 && res[1] === 1601 && res[2] === 3195 && res[3] === 1598);
assert.equal(res[0], 3198);
assert.equal(res[1], 1601);
assert.equal(res[2], 3195);
assert.equal(res[3], 1598);
}
};
}
export async function mol_bench_owned() {
let run, destroy;
const destroy_owned = $.effect_root(() => {
// Do 10 loops to warm up JIT
for (let i = 0; i < 10; i++) {
const { run, destroy } = setup();
run(0);
destroy();
}
({ run, destroy } = setup());
});
const { timing } = await fastest_test(10, () => {
for (let i = 0; i < 1e4; i++) {
run(i);
}
});
// @ts-ignore
destroy();
destroy_owned();
return {
benchmark: 'mol_bench_owned',
time: timing.time.toFixed(2),
gc_time: timing.gc_time.toFixed(2)
};
}
export async function mol_bench_unowned() {
// Do 10 loops to warm up JIT
for (let i = 0; i < 10; i++) {
const { run, destroy } = setup();
run(0);
destroy();
}
const { run, destroy } = setup();
const { timing } = await fastest_test(10, () => {
for (let i = 0; i < 1e4; i++) {
run(i);
}
});
destroy();
return {
benchmark: 'mol_bench_unowned',
time: timing.time.toFixed(2),
gc_time: timing.gc_time.toFixed(2)
};
}
};

@ -0,0 +1,35 @@
import assert from 'node:assert';
import * as $ from 'svelte/internal/client';
const ARRAY_SIZE = 1000;
export default () => {
const signals = Array.from({ length: ARRAY_SIZE }, (_, i) => $.state(i));
const order = $.state(0);
// break skipped_deps fast path by changing order of reads
const total = $.derived(() => {
const ord = $.get(order);
let sum = 0;
for (let i = 0; i < ARRAY_SIZE; i++) {
sum += /** @type {number} */ ($.get(signals[(i + ord) % ARRAY_SIZE]));
}
return sum;
});
const destroy = $.effect_root(() => {
$.render_effect(() => {
$.get(total);
});
});
return {
destroy,
run() {
for (let i = 0; i < 5; i++) {
$.flush(() => $.set(order, i));
assert.equal($.get(total), (ARRAY_SIZE * (ARRAY_SIZE - 1)) / 2); // sum of 0..999
}
}
};
};

@ -0,0 +1,71 @@
import * as $ from 'svelte/internal/client';
import { fastest_test } from '../../utils.js';
export function busy() {
let a = 0;
for (let i = 0; i < 1_00; i++) {
a++;
}
}
/**
*
* @param {string} label
* @param {() => { run: (i?: number) => void, destroy: () => void }} setup
*/
export function create_test(label, setup) {
return {
unowned: {
label: `${label}_unowned`,
fn: async () => {
// Do 10 loops to warm up JIT
for (let i = 0; i < 10; i++) {
const { run, destroy } = setup();
run(0);
destroy();
}
const { run, destroy } = setup();
const result = await fastest_test(10, () => {
for (let i = 0; i < 1000; i++) {
run(i);
}
});
destroy();
return result;
}
},
owned: {
label: `${label}_owned`,
fn: async () => {
let run, destroy;
const destroy_owned = $.effect_root(() => {
// Do 10 loops to warm up JIT
for (let i = 0; i < 10; i++) {
const { run, destroy } = setup();
run(0);
destroy();
}
({ run, destroy } = setup());
});
const result = await fastest_test(10, () => {
for (let i = 0; i < 1000; i++) {
run(i);
}
});
// @ts-ignore
destroy();
destroy_owned();
return result;
}
}
};
}

@ -1,13 +1,16 @@
import * as fs from 'node:fs';
import * as path from 'node:path';
import { render } from 'svelte/server';
import { fastest_test, read_file, write } from '../../../utils.js';
import { fastest_test } from '../../../utils.js';
import { compile } from 'svelte/compiler';
const dir = `${process.cwd()}/benchmarking/benchmarks/ssr/wrapper`;
async function compile_svelte() {
const output = compile(read_file(`${dir}/App.svelte`), {
const output = compile(read(`${dir}/App.svelte`), {
generate: 'server'
});
write(`${dir}/output/App.js`, output.js.code);
const module = await import(`${dir}/output/App.js`);
@ -15,22 +18,39 @@ async function compile_svelte() {
return module.default;
}
export async function wrapper_bench() {
const App = await compile_svelte();
// Do 3 loops to warm up JIT
for (let i = 0; i < 3; i++) {
render(App);
}
export const wrapper_bench = {
label: 'wrapper_bench',
fn: async () => {
const App = await compile_svelte();
const { timing } = await fastest_test(10, () => {
for (let i = 0; i < 100; i++) {
// Do 3 loops to warm up JIT
for (let i = 0; i < 3; i++) {
render(App);
}
});
return {
benchmark: 'wrapper_bench',
time: timing.time.toFixed(2),
gc_time: timing.gc_time.toFixed(2)
};
return await fastest_test(10, () => {
for (let i = 0; i < 100; i++) {
render(App);
}
});
}
};
/**
* @param {string} file
*/
function read(file) {
return fs.readFileSync(file, 'utf-8').replace(/\r\n/g, '\n');
}
/**
* @param {string} file
* @param {string} contents
*/
function write(file, contents) {
try {
fs.mkdirSync(path.dirname(file), { recursive: true });
} catch {}
fs.writeFileSync(file, contents);
}

@ -1,10 +1,13 @@
import { reactivity_benchmarks } from '../benchmarks/reactivity/index.js';
const results = [];
for (const benchmark of reactivity_benchmarks) {
const result = await benchmark();
console.error(result.benchmark);
results.push(result);
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()) });
process.stderr.write('\x1b[2K\r');
}
process.send(results);

@ -2,54 +2,86 @@ 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';
let total_time = 0;
let total_gc_time = 0;
// e.g. `pnpm bench kairo` to only run the kairo benchmarks
const filters = process.argv.slice(2);
const suites = [
{ benchmarks: reactivity_benchmarks, name: 'reactivity benchmarks' },
{ benchmarks: ssr_benchmarks, name: 'server-side rendering benchmarks' }
];
{
benchmarks: reactivity_benchmarks.filter(
(b) => filters.length === 0 || filters.some((f) => b.label.includes(f))
),
name: 'reactivity benchmarks'
},
{
benchmarks: ssr_benchmarks.filter(
(b) => filters.length === 0 || filters.some((f) => b.label.includes(f))
),
name: 'server-side rendering benchmarks'
}
].filter((suite) => suite.benchmarks.length > 0);
if (suites.length === 0) {
console.log('No benchmarks matched provided filters');
process.exit(1);
}
const COLUMN_WIDTHS = [25, 9, 9];
const TOTAL_WIDTH = COLUMN_WIDTHS.reduce((a, b) => a + b);
const pad_right = (str, n) => str + ' '.repeat(n - str.length);
const pad_left = (str, n) => ' '.repeat(n - str.length) + str;
let total_time = 0;
let total_gc_time = 0;
// eslint-disable-next-line no-console
console.log('\x1b[1m', '-- Benchmarking Started --', '\x1b[0m');
$.push({}, true);
try {
for (const { benchmarks, name } of suites) {
let suite_time = 0;
let suite_gc_time = 0;
// eslint-disable-next-line no-console
console.log(`\nRunning ${name}...\n`);
console.log(
pad_right('Benchmark', COLUMN_WIDTHS[0]) +
pad_left('Time', COLUMN_WIDTHS[1]) +
pad_left('GC time', COLUMN_WIDTHS[2])
);
console.log('='.repeat(TOTAL_WIDTH));
for (const benchmark of benchmarks) {
const results = await benchmark();
// eslint-disable-next-line no-console
console.log(results);
total_time += Number(results.time);
total_gc_time += Number(results.gc_time);
suite_time += Number(results.time);
suite_gc_time += Number(results.gc_time);
const results = await benchmark.fn();
console.log(
pad_right(benchmark.label, COLUMN_WIDTHS[0]) +
pad_left(results.time.toFixed(2), COLUMN_WIDTHS[1]) +
pad_left(results.gc_time.toFixed(2), COLUMN_WIDTHS[2])
);
total_time += results.time;
total_gc_time += results.gc_time;
suite_time += results.time;
suite_gc_time += results.gc_time;
}
console.log(`\nFinished ${name}.\n`);
// eslint-disable-next-line no-console
console.log({
suite_time: suite_time.toFixed(2),
suite_gc_time: suite_gc_time.toFixed(2)
});
console.log('='.repeat(TOTAL_WIDTH));
console.log(
pad_right('suite', COLUMN_WIDTHS[0]) +
pad_left(suite_time.toFixed(2), COLUMN_WIDTHS[1]) +
pad_left(suite_gc_time.toFixed(2), COLUMN_WIDTHS[2])
);
console.log('='.repeat(TOTAL_WIDTH));
}
} catch (e) {
// eslint-disable-next-line no-console
console.log('\x1b[1m', '\n-- Benchmarking Failed --\n', '\x1b[0m');
// eslint-disable-next-line no-console
console.error(e);
process.exit(1);
}
$.pop();
// eslint-disable-next-line no-console
console.log('\x1b[1m', '\n-- Benchmarking Complete --\n', '\x1b[0m');
// eslint-disable-next-line no-console
console.log({
total_time: total_time.toFixed(2),
total_gc_time: total_gc_time.toFixed(2)
});
console.log('');
console.log(
pad_right('total', COLUMN_WIDTHS[0]) +
pad_left(total_time.toFixed(2), COLUMN_WIDTHS[1]) +
pad_left(total_gc_time.toFixed(2), COLUMN_WIDTHS[2])
);

@ -1,78 +1,30 @@
import { performance, PerformanceObserver } from 'node:perf_hooks';
import v8 from 'v8-natives';
import * as fs from 'node:fs';
import * as path from 'node:path';
// Credit to https://github.com/milomg/js-reactivity-benchmark for the logic for timing + GC tracking.
class GarbageTrack {
track_id = 0;
observer = new PerformanceObserver((list) => this.perf_entries.push(...list.getEntries()));
perf_entries = [];
periods = [];
async function track(fn) {
v8.collectGarbage();
watch(fn) {
this.track_id++;
const start = performance.now();
const result = fn();
const end = performance.now();
this.periods.push({ track_id: this.track_id, start, end });
/** @type {PerformanceEntry[]} */
const entries = [];
return { result, track_id: this.track_id };
}
const observer = new PerformanceObserver((list) => entries.push(...list.getEntries()));
observer.observe({ entryTypes: ['gc'] });
/**
* @param {number} track_id
*/
async gcDuration(track_id) {
await promise_delay(10);
const start = performance.now();
fn();
const end = performance.now();
const period = this.periods.find((period) => period.track_id === track_id);
if (!period) {
// eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors
return Promise.reject('no period found');
}
await new Promise((f) => setTimeout(f, 10));
const entries = this.perf_entries.filter(
(e) => e.startTime >= period.start && e.startTime < period.end
);
return entries.reduce((t, e) => e.duration + t, 0);
}
const gc_time = entries
.filter((e) => e.startTime >= start && e.startTime < end)
.reduce((t, e) => e.duration + t, 0);
destroy() {
this.observer.disconnect();
}
observer.disconnect();
constructor() {
this.observer.observe({ entryTypes: ['gc'] });
}
}
function promise_delay(timeout = 0) {
return new Promise((resolve) => setTimeout(resolve, timeout));
}
/**
* @param {{ (): void; (): any; }} fn
*/
function run_timed(fn) {
const start = performance.now();
const result = fn();
const time = performance.now() - start;
return { result, time };
}
/**
* @param {() => void} fn
*/
async function run_tracked(fn) {
v8.collectGarbage();
const gc_track = new GarbageTrack();
const { result: wrappedResult, track_id } = gc_track.watch(() => run_timed(fn));
const gc_time = await gc_track.gcDuration(track_id);
const { result, time } = wrappedResult;
gc_track.destroy();
return { result, timing: { time, gc_time } };
return { time: end - start, gc_time };
}
/**
@ -80,40 +32,12 @@ async function run_tracked(fn) {
* @param {() => void} fn
*/
export async function fastest_test(times, fn) {
/** @type {Array<{ time: number, gc_time: number }>} */
const results = [];
for (let i = 0; i < times; i++) {
const run = await run_tracked(fn);
results.push(run);
}
const fastest = results.reduce((a, b) => (a.timing.time < b.timing.time ? a : b));
return fastest;
}
/**
* @param {boolean} a
*/
export function assert(a) {
if (!a) {
throw new Error('Assertion failed');
for (let i = 0; i < times; i++) {
results.push(await track(fn));
}
}
/**
* @param {string} file
*/
export function read_file(file) {
return fs.readFileSync(file, 'utf-8').replace(/\r\n/g, '\n');
}
/**
* @param {string} file
* @param {string} contents
*/
export function write(file, contents) {
try {
fs.mkdirSync(path.dirname(file), { recursive: true });
} catch {}
fs.writeFileSync(file, contents);
return results.reduce((a, b) => (a.time < b.time ? a : b));
}

@ -15,17 +15,18 @@ Don't worry if you don't know Svelte yet! You can ignore all the nice features S
## Alternatives to SvelteKit
You can also use Svelte directly with Vite by running `npm create vite@latest` and selecting the `svelte` option. With this, `npm run build` will generate HTML, JS, and CSS files inside the `dist` directory using [vite-plugin-svelte](https://github.com/sveltejs/vite-plugin-svelte). In most cases, you will probably need to [choose a routing library](faq#Is-there-a-router) as well.
You can also use Svelte directly with Vite via [vite-plugin-svelte](https://github.com/sveltejs/vite-plugin-svelte) by running `npm create vite@latest` and selecting the `svelte` option (or, if working with an existing project, adding the plugin to your `vite.config.js` file). With this, `npm run build` will generate HTML, JS, and CSS files inside the `dist` directory. In most cases, you will probably need to [choose a routing library](/packages#routing) as well.
>[!NOTE] Vite is often used in standalone mode to build [single page apps (SPAs)](../kit/glossary#SPA), which you can also [build with SvelteKit](../kit/single-page-apps).
There are also plugins for [Rollup](https://github.com/sveltejs/rollup-plugin-svelte), [Webpack](https://github.com/sveltejs/svelte-loader) [and a few others](https://sveltesociety.dev/packages?category=build-plugins), but we recommend Vite.
There are also [plugins for other bundlers](/packages#bundler-plugins), but we recommend Vite.
## Editor tooling
The Svelte team maintains a [VS Code extension](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode), and there are integrations with various other [editors](https://sveltesociety.dev/resources#editor-support) and tools as well.
The Svelte team maintains a [VS Code extension](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode), and there are integrations with various other [editors](https://sveltesociety.dev/collection/editor-support-c85c080efc292a34) and tools as well.
You can also check your code from the command line using [`npx sv check`](https://svelte.dev/docs/cli/sv-check).
You can also check your code from the command line using [sv check](https://github.com/sveltejs/cli).
## Getting help

@ -166,6 +166,21 @@ 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`.
## `$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).
In some cases, you may want to update the UI as soon as the state changes. For example, you might want to update a navigation bar when the user clicks on a link, so that they get visual feedback while waiting for the new page to load. To do this, use `$state.eager(value)`:
```svelte
<nav>
<a href="/" aria-current={$state.eager(pathname) === '/' ? 'page' : null}>home</a>
<a href="/about" aria-current={$state.eager(pathname) === '/about' ? 'page' : null}>about</a>
</nav>
```
Use this feature sparingly, and only to provide feedback in response to user action — in general, allowing Svelte to coordinate updates will provide a better user experience.
## Passing state into functions
JavaScript is a _pass-by-value_ language — when you call a function, the arguments are the _values_ rather than the _variables_. In other words:

@ -85,8 +85,9 @@ Derived expressions are recalculated when their dependencies change, but you can
Unlike `$state`, which converts objects and arrays to [deeply reactive proxies]($state#Deep-state), `$derived` values are left as-is. For example, [in a case like this](/playground/untitled#H4sIAAAAAAAAE4VU22rjMBD9lUHd3aaQi9PdstS1A3t5XvpQ2Ic4D7I1iUUV2UjjNMX431eS7TRdSosxgjMzZ45mjt0yzffIYibvy0ojFJWqDKCQVBk2ZVup0LJ43TJ6rn2aBxw-FP2o67k9oCKP5dziW3hRaUJNjoYltjCyplWmM1JIIAn3FlL4ZIkTTtYez6jtj4w8WwyXv9GiIXiQxLVs9pfTMR7EuoSLIuLFbX7Z4930bZo_nBrD1bs834tlfvsBz9_SyX6PZXu9XaL4gOWn4sXjeyzftv4ZWfyxubpzxzg6LfD4MrooxELEosKCUPigQCMPKCZh0OtQE1iSxcsmdHuBvCiHZXALLXiN08EL3RRkaJ_kDVGle0HcSD5TPEeVtj67O4Nrg9aiSNtBY5oODJkrL5QsHtN2cgXp6nSJMWzpWWGasdlsGEMbzi5jPr5KFr0Ep7pdeM2-TCelCddIhDxAobi1jqF3cMaC1RKp64bAW9iFAmXGIHfd4wNXDabtOLN53w8W53VvJoZLh7xk4Rr3CoL-UNoLhWHrT1JQGcM17u96oES5K-kc2XOzkzqGCKL5De79OUTyyrg1zgwXsrEx3ESfx4Bz0M5UjVMHB24mw9SuXtXFoN13fYKOM1tyUT3FbvbWmSWCZX2Er-41u5xPoml45svRahl9Wb9aasbINJixDZwcPTbyTLZSUsAvrg_cPuCR7s782_WU8343Y72Qtlb8OYatwuOQvuN13M_hJKNfxann1v1U_B1KZ_D_mzhzhz24fw85CSz2irtN9w9HshBK7AQAAA==)...
```svelte
let items = $state([...]);
```js
// @errors: 7005
let items = $state([ /*...*/ ]);
let index = $state(0);
let selected = $derived(items[index]);

@ -198,6 +198,8 @@ You can, of course, separate the type declaration from the annotation:
> [!NOTE] Interfaces for native DOM elements are provided in the `svelte/elements` module (see [Typing wrapper components](typescript#Typing-wrapper-components))
If your component exposes [snippet](snippet) props like `children`, these should be typed using the `Snippet` interface imported from `'svelte'` — see [Typing snippets](snippet#Typing-snippets) for examples.
Adding types is recommended, as it ensures that people using your component can easily discover which props they should provide.

@ -4,7 +4,7 @@ title: $bindable
Ordinarily, props go one way, from parent to child. This makes it easy to understand how data flows around your app.
In Svelte, component props can be _bound_, which means that data can also flow _up_ from child to parent. This isn't something you should do often, but it can simplify your code if used sparingly and carefully.
In Svelte, component props can be _bound_, which means that data can also flow _up_ from child to parent. This isn't something you should do often — overuse can make your data flow unpredictable and your components harder to maintain — but it can simplify your code if used sparingly and carefully.
It also means that a state proxy can be _mutated_ in the child.

@ -18,6 +18,8 @@ The `$inspect` rune is roughly equivalent to `console.log`, with the exception t
<input bind:value={message} />
```
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)):
@ -36,13 +38,6 @@ The `$inspect` rune is roughly equivalent to `console.log`, with the exception t
<button onclick={() => count++}>Increment</button>
```
A convenient way to find the origin of some change is to pass `console.trace` to `with`:
```js
// @errors: 2304
$inspect(stuff).with(console.trace);
```
## $inspect.trace(...)
This rune, added in 5.14, causes the surrounding function to be _traced_ in development. Any time the function re-runs as part of an [effect]($effect) or a [derived]($derived), information will be printed to the console about which pieces of reactive state caused the effect to fire.

@ -12,7 +12,9 @@ title: {#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`— in other words, anything that can be used with `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).
```svelte
<h1>Shopping list</h1>

@ -95,7 +95,7 @@ Since 5.6.0, if an `<input>` has a `defaultValue` and is part of a form, it will
## `<input bind:checked>`
Checkbox and radio inputs can be bound with `bind:checked`:
Checkbox inputs can be bound with `bind:checked`:
```svelte
<label>
@ -117,6 +117,8 @@ Since 5.6.0, if an `<input>` has a `defaultChecked` attribute and is part of a f
</form>
```
> [!NOTE] Use `bind:group` for radio inputs instead of `bind:checked`.
## `<input bind:indeterminate>`
Checkboxes can be in an [indeterminate](https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/indeterminate) state, independently of whether they are checked or unchecked:
@ -362,6 +364,8 @@ Components also support `bind:this`, allowing you to interact with component ins
</script>
```
> [!NOTE] In case of using [the function bindings](#Function-bindings), the getter is required to ensure that the correct value is nullified on component or element destruction.
## bind:_property_ for components
```svelte

@ -23,24 +23,6 @@ export default {
The experimental flag will be removed in Svelte 6.
## Boundaries
Currently, you can only use `await` inside a [`<svelte:boundary>`](svelte-boundary) with a `pending` snippet:
```svelte
<svelte:boundary>
<MyApp />
{#snippet pending()}
<p>loading...</p>
{/snippet}
</svelte:boundary>
```
This restriction will be lifted once Svelte supports asynchronous server-side rendering (see [caveats](#Caveats)).
> [!NOTE] In the [playground](/playground), your app is rendered inside a boundary with an empty pending snippet, so that you can use `await` without having to create one.
## 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)...
@ -99,7 +81,9 @@ let b = $derived(await two());
## Indicating loading states
In addition to the nearest boundary's [`pending`](svelte-boundary#Properties-pending) snippet, you can indicate that asynchronous work is ongoing with [`$effect.pending()`]($effect#$effect.pending).
To render placeholder UI, you can wrap content in a `<svelte:boundary>` with a [`pending`](svelte-boundary#Properties-pending) snippet. This will be shown when the boundary is first created, but not for subsequent updates, which are globally coordinated.
After the contents of a boundary have resolved for the first time and have replaced the `pending` snippet, you can detect subsequent async work with [`$effect.pending()`]($effect#$effect.pending). This is what you would use to display a "we're asynchronously validating your input" spinner next to a form field, for example.
You can also use [`settled()`](svelte#settled) to get a promise that resolves when the current update is complete:
@ -133,12 +117,76 @@ async function onclick() {
Errors in `await` expressions will bubble to the nearest [error boundary](svelte-boundary).
## Server-side rendering
Svelte supports asynchronous server-side rendering (SSR) with the `render(...)` API. To use it, simply await the return value:
```js
/// file: server.js
import { render } from 'svelte/server';
import App from './App.svelte';
const { head, body } = +++await+++ render(App);
```
> [!NOTE] If you're using a framework like SvelteKit, this is done on your behalf.
If a `<svelte:boundary>` with a `pending` snippet is encountered during SSR, that snippet will be rendered while the rest of the content is ignored. All `await` expressions encountered outside boundaries with `pending` snippets will resolve and render their contents prior to `await render(...)` returning.
> [!NOTE] In the future, we plan to add a streaming implementation that renders the content in the background.
## Forking
The [`fork(...)`](svelte#fork) API, added in 5.42, makes it possible to run `await` expressions that you _expect_ to happen in the near future. This is mainly intended for frameworks like SvelteKit to implement preloading when (for example) users signal an intent to navigate.
```svelte
<script>
import { fork } from 'svelte';
import Menu from './Menu.svelte';
let open = $state(false);
/** @type {import('svelte').Fork | null} */
let pending = null;
function preload() {
pending ??= fork(() => {
open = true;
});
}
function discard() {
pending?.discard();
pending = null;
}
</script>
<button
onfocusin={preload}
onfocusout={discard}
onpointerenter={preload}
onpointerleave={discard}
onclick={() => {
pending?.commit();
pending = null;
// in case `pending` didn't exist
// (if it did, this is a no-op)
open = true;
}}
>open menu</button>
{#if open}
<!-- any async work inside this component will start
as soon as the fork is created -->
<Menu onclose={() => open = false} />
{/if}
```
## Caveats
As an experimental feature, the details of how `await` is handled (and related APIs like `$effect.pending()`) are subject to breaking changes outside of a semver major release, though we intend to keep such changes to a bare minimum.
Currently, server-side rendering is synchronous. If a `<svelte:boundary>` with a `pending` snippet is encountered during SSR, only the `pending` snippet will be rendered.
## Breaking changes
Effects run in a slightly different order when the `experimental.async` option is `true`. Specifically, _block_ effects like `{#if ...}` and `{#each ...}` now run before an `$effect.pre` or `beforeUpdate` in the same component, which means that in [very rare situations](/playground/untitled?#H4sIAAAAAAAAE22R3VLDIBCFX2WLvUhnTHsf0zre-Q7WmfwtFV2BgU1rJ5N3F0jaOuoVcPbw7VkYhK4_URTiGYkMnIyjDjLsFGO3EvdCKkIvipdB8NlGXxSCPt96snbtj0gctab2-J_eGs2oOWBE6VunLO_2es-EDKZ5x5ZhC0vPNWM2gHXGouNzAex6hHH1cPHil_Lsb95YT9VQX6KUAbS2DrNsBdsdDFHe8_XSYjH1SrhELTe3MLpsemajweiWVPuxHSbKNd-8eQTdE0EBf4OOaSg2hwNhhE_ABB_ulJzjj9FULvIcqgm5vnAqUB7wWFMfhuugQWkcAr8hVD-mq8D12kOep24J_IszToOXdveGDsuNnZwbJUNlXsKnhJdhUcTo42s41YpOSneikDV5HL8BktM6yRcCAAA=) it is possible to update a block that should no longer exist, but only if you update state inside an effect, [which you should avoid]($effect#When-not-to-use-$effect).

@ -24,7 +24,7 @@ For the boundary to do anything, one or more of the following must be provided.
### `pending`
As of Svelte 5.36, boundaries with a `pending` snippet can contain [`await`](await-expressions) expressions. This snippet will be shown when the boundary is first created, and will remain visible until all the `await` expressions inside the boundary have resolved ([demo](/playground/untitled#H4sIAAAAAAAAE21QQW6DQAz8ytY9BKQVpFdKkPqDHnorPWzAaSwt3tWugUaIv1eE0KpKD5as8YxnNBOw6RAKKOOAVrA4up5bEy6VGknOyiO3xJ8qMnmPAhpOZDFC8T6BXPyiXADQ258X77P1FWg4moj_4Y1jQZZ49W0CealqruXUcyPkWLVozQXbZDC2R606spYiNo7bqA7qab_fp2paFLUElD6wYhzVa3AdRUySgNHZAVN1qDZaLRHljTp0vSTJ9XJjrSbpX5f0eZXN6zLXXOa_QfmurIVU-moyoyH5ib87o7XuYZfOZe6vnGWmx1uZW7lJOq9upa-sMwuUZdkmmfIbfQ1xZwwaBL8ECgk9zh8axJAdiVsoTsZGnL8Bg4tX_OMBAAA=)):
This snippet will be shown when the boundary is first created, and will remain visible until all the [`await`](await-expressions) expressions inside the boundary have resolved ([demo](/playground/untitled#H4sIAAAAAAAAE21QQW6DQAz8ytY9BKQVpFdKkPqDHnorPWzAaSwt3tWugUaIv1eE0KpKD5as8YxnNBOw6RAKKOOAVrA4up5bEy6VGknOyiO3xJ8qMnmPAhpOZDFC8T6BXPyiXADQ258X77P1FWg4moj_4Y1jQZZ49W0CealqruXUcyPkWLVozQXbZDC2R606spYiNo7bqA7qab_fp2paFLUElD6wYhzVa3AdRUySgNHZAVN1qDZaLRHljTp0vSTJ9XJjrSbpX5f0eZXN6zLXXOa_QfmurIVU-moyoyH5ib87o7XuYZfOZe6vnGWmx1uZW7lJOq9upa-sMwuUZdkmmfIbfQ1xZwwaBL8ECgk9zh8axJAdiVsoTsZGnL8Bg4tX_OMBAAA=)):
```svelte
<svelte:boundary>

@ -83,27 +83,18 @@ Svelte will warn you if you get it wrong.
## Type-safe context
A useful pattern is to wrap the calls to `setContext` and `getContext` inside helper functions that let you preserve type safety:
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:
```js
/// file: context.js
```ts
/// file: context.ts
// @filename: ambient.d.ts
interface User {}
// @filename: index.js
// @filename: index.ts
// ---cut---
import { getContext, setContext } from 'svelte';
const key = {};
/** @param {User} user */
export function setUserContext(user) {
setContext(key, user);
}
import { createContext } from 'svelte';
export function getUserContext() {
return /** @type {User} */ (getContext(key));
}
export const [getUserContext, setUserContext] = createContext<User>();
```
## Replacing global state

@ -94,7 +94,7 @@ Svelte 4 contained hooks that ran before and after the component as a whole was
</script>
```
Instead of `beforeUpdate` use `$effect.pre` and instead of `afterUpdate` use `$effect` instead - these runes offer more granular control and only react to the changes you're actually interested in.
Instead of `beforeUpdate` use `$effect.pre` and instead of `afterUpdate` use `$effect` instead these runes offer more granular control and only react to the changes you're actually interested in.
### Chat window example

@ -0,0 +1,123 @@
---
title: Hydratable data
---
In Svelte, when you want to render asynchronous content data on the server, you can simply `await` it. This is great! However, it comes with a pitfall: when hydrating that content on the client, Svelte has to redo the asynchronous work, which blocks hydration for however long it takes:
```svelte
<script>
import { getUser } from 'my-database-library';
// This will get the user on the server, render the user's name into the h1,
// and then, during hydration on the client, it will get the user _again_,
// blocking hydration until it's done.
const user = await getUser();
</script>
<h1>{user.name}</h1>
```
That's silly, though. If we've already done the hard work of getting the data on the server, we don't want to get it again during hydration on the client. `hydratable` is a low-level API built to solve this problem. You probably won't need this very often — it will be used behind the scenes by whatever datafetching library you use. For example, it powers [remote functions in SvelteKit](/docs/kit/remote-functions).
To fix the example above:
```svelte
<script>
import { hydratable } from 'svelte';
import { getUser } from 'my-database-library';
// During server rendering, this will serialize and stash the result of `getUser`, associating
// it with the provided key and baking it into the `head` content. During hydration, it will
// look for the serialized version, returning it instead of running `getUser`. After hydration
// is done, if it's called again, it'll simply invoke `getUser`.
const user = await hydratable('user', () => getUser());
</script>
<h1>{user.name}</h1>
```
This API can also be used to provide access to random or time-based values that are stable between server rendering and hydration. For example, to get a random number that doesn't update on hydration:
```ts
import { hydratable } from 'svelte';
const rand = hydratable('random', () => Math.random());
```
If you're a library author, be sure to prefix the keys of your `hydratable` values with the name of your library so that your keys don't conflict with other libraries.
## Serialization
All data returned from a `hydratable` function must be serializable. But this doesn't mean you're limited to JSON — Svelte uses [`devalue`](https://npmjs.com/package/devalue), which can serialize all sorts of things including `Map`, `Set`, `URL`, and `BigInt`. Check the documentation page for a full list. In addition to these, thanks to some Svelte magic, you can also fearlessly use promises:
```svelte
<script>
import { hydratable } from 'svelte';
const promises = hydratable('random', () => {
return {
one: Promise.resolve(1),
two: Promise.resolve(2)
}
});
</script>
{await promises.one}
{await promises.two}
```
## CSP
`hydratable` adds an inline `<script>` block to the `head` returned from `render`. If you're using [Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CSP) (CSP), this script will likely fail to run. You can provide a `nonce` to `render`:
```js
/// file: server.js
import { render } from 'svelte/server';
import App from './App.svelte';
// ---cut---
const nonce = crypto.randomUUID();
const { head, body } = await render(App, {
csp: { nonce }
});
```
This will add the `nonce` to the script block, on the assumption that you will later add the same nonce to the CSP header of the document that contains it:
```js
/// file: server.js
let response = new Response();
let nonce = 'xyz123';
// ---cut---
response.headers.set(
'Content-Security-Policy',
`script-src 'nonce-${nonce}'`
);
```
It's essential that a `nonce` — which, British slang definition aside, means 'number used once' — is only used when dynamically server rendering an individual response.
If instead you are generating static HTML ahead of time, you must use hashes instead:
```js
/// file: server.js
import { render } from 'svelte/server';
import App from './App.svelte';
// ---cut---
const { head, body, hashes } = await render(App, {
csp: { hash: true }
});
```
`hashes.script` will be an array of strings like `["sha256-abcd123"]`. As with `nonce`, the hashes should be used in your CSP header:
```js
/// file: server.js
let response = new Response();
let hashes = { script: ['sha256-xyz123'] };
// ---cut---
response.headers.set(
'Content-Security-Policy',
`script-src ${hashes.script.map((hash) => `'${hash}'`).join(' ')}`
);
```
We recommend using `nonce` over hash if you can, as `hash` will interfere with streaming SSR in the future.

@ -4,7 +4,7 @@ title: Testing
Testing helps you write and maintain your code and guard against regressions. Testing frameworks help you with that, allowing you to describe assertions or expectations about how your code should behave. Svelte is unopinionated about which testing framework you use — you can write unit tests, integration tests, and end-to-end tests using solutions like [Vitest](https://vitest.dev/), [Jasmine](https://jasmine.github.io/), [Cypress](https://www.cypress.io/) and [Playwright](https://playwright.dev/).
## Unit and integration testing using Vitest
## Unit and component tests with Vitest
Unit tests allow you to test small isolated parts of your code. Integration tests allow you to test parts of your application to see if they work together. If you're using Vite (including via SvelteKit), we recommend using [Vitest](https://vitest.dev/). You can use the Svelte CLI to [setup Vitest](/docs/cli/vitest) either during project creation or later on.
@ -246,7 +246,7 @@ test('Component', async () => {
When writing component tests that involve two-way bindings, context or snippet props, it's best to create a wrapper component for your specific test and interact with that. `@testing-library/svelte` contains some [examples](https://testing-library.com/docs/svelte-testing-library/example).
### Component testing with Storybook
## Component tests with Storybook
[Storybook](https://storybook.js.org) is a tool for developing and documenting UI components, and it can also be used to test your components. They're run with Vitest's browser mode, which renders your components in a real browser for the most realistic testing environment.
@ -288,13 +288,13 @@ You can create stories for component variations and test interactions with the [
/>
```
## E2E tests using Playwright
## End-to-end tests with Playwright
E2E (short for 'end to end') tests allow you to test your full application through the eyes of the user. This section uses [Playwright](https://playwright.dev/) as an example, but you can also use other solutions like [Cypress](https://www.cypress.io/) or [NightwatchJS](https://nightwatchjs.org/).
You can use the Svelte CLI to [setup Playwright](/docs/cli/playwright) either during project creation or later on. You can also [set it up with `npm init playwright`](https://playwright.dev/docs/intro). Additionally, you may also want to install an IDE plugin such as [the VS Code extension](https://playwright.dev/docs/getting-started-vscode) to be able to execute tests from inside your IDE.
If you've run `npm init playwright` or are not using Vite, you may need to adjust the Playwright config to tell Playwright what to do before running the tests - mainly starting your application at a certain port. For example:
If you've run `npm init playwright` or are not using Vite, you may need to adjust the Playwright config to tell Playwright what to do before running the tests mainly starting your application at a certain port. For example:
```js
/// file: playwright.config.js

@ -66,7 +66,10 @@ The inner Svelte component is destroyed in the next tick after the `disconnected
When constructing a custom element, you can tailor several aspects by defining `customElement` as an object within `<svelte:options>` since Svelte 4. This object may contain the following properties:
- `tag: string`: an optional `tag` property for the custom element's name. If set, a custom element with this tag name will be defined with the document's `customElements` registry upon importing this component.
- `shadow`: an optional property that can be set to `"none"` to forgo shadow root creation. Note that styles are then no longer encapsulated, and you can't use slots
- `shadow`: an optional property to modify shadow root properties. It accepts the following values:
- `"none"`: No shadow root is created. Note that styles are then no longer encapsulated, and you can't use slots.
- `"open"`: Shadow root is created with the `mode: "open"` option.
- [`ShadowRootInit`](https://developer.mozilla.org/en-US/docs/Web/API/Element/attachShadow#options): You can pass a settings object that will be passed to `attachShadow()` when shadow root is created.
- `props`: an optional property to modify certain details and behaviors of your component's properties. It offers the following settings:
- `attribute: string`: To update a custom element's prop, you have two alternatives: either set the property on the custom element's reference as illustrated above or use an HTML attribute. For the latter, the default attribute name is the lowercase property name. Modify this by assigning `attribute: "<desired name>"`.
- `reflect: boolean`: By default, updated prop values do not reflect back to the DOM. To enable this behavior, set `reflect: true`.
@ -78,7 +81,11 @@ When constructing a custom element, you can tailor several aspects by defining `
<svelte:options
customElement={{
tag: 'custom-element',
shadow: 'none',
shadow: {
mode: import.meta.env.DEV ? 'open' : 'closed',
clonable: true,
// ...
},
props: {
name: { reflect: true, type: 'Number', attribute: 'element-index' }
},

@ -137,7 +137,7 @@ Transitions are now local by default to prevent confusion around page navigation
{/if}
```
To make transitions global, add the `|global` modifier - then they will play when _any_ control flow block above is created/destroyed. The migration script will do this automatically for you. ([#6686](https://github.com/sveltejs/svelte/issues/6686))
To make transitions global, add the `|global` modifier then they will play when _any_ control flow block above is created/destroyed. The migration script will do this automatically for you. ([#6686](https://github.com/sveltejs/svelte/issues/6686))
## Default slot bindings
@ -150,10 +150,10 @@ Default slot bindings are no longer exposed to named slots and vice versa:
<Nested let:count>
<p>
count in default slot - is available: {count}
count in default slot is available: {count}
</p>
<p slot="bar">
count in bar slot - is not available: {count}
count in bar slot is not available: {count}
</p>
</Nested>
```

@ -4,7 +4,7 @@ title: Svelte 5 migration guide
Version 5 comes with an overhauled syntax and reactivity system. While it may look different at first, you'll soon notice many similarities. This guide goes over the changes in detail and shows you how to upgrade. Along with it, we also provide information on _why_ we did these changes.
You don't have to migrate to the new syntax right away - Svelte 5 still supports the old Svelte 4 syntax, and you can mix and match components using the new syntax with components using the old and vice versa. We expect many people to be able to upgrade with only a few lines of code changed initially. There's also a [migration script](#Migration-script) that helps you with many of these steps automatically.
You don't have to migrate to the new syntax right away Svelte 5 still supports the old Svelte 4 syntax, and you can mix and match components using the new syntax with components using the old and vice versa. We expect many people to be able to upgrade with only a few lines of code changed initially. There's also a [migration script](#Migration-script) that helps you with many of these steps automatically.
## Reactivity syntax changes
@ -23,7 +23,7 @@ In Svelte 4, a `let` declaration at the top level of a component was implicitly
Nothing else changes. `count` is still the number itself, and you read and write directly to it, without a wrapper like `.value` or `getCount()`.
> [!DETAILS] Why we did this
> `let` being implicitly reactive at the top level worked great, but it meant that reactivity was constrained - a `let` declaration anywhere else was not reactive. This forced you to resort to using stores when refactoring code out of the top level of components for reuse. This meant you had to learn an entirely separate reactivity model, and the result often wasn't as nice to work with. Because reactivity is more explicit in Svelte 5, you can keep using the same API outside the top level of components. Head to [the tutorial](/tutorial) to learn more.
> `let` being implicitly reactive at the top level worked great, but it meant that reactivity was constrained a `let` declaration anywhere else was not reactive. This forced you to resort to using stores when refactoring code out of the top level of components for reuse. This meant you had to learn an entirely separate reactivity model, and the result often wasn't as nice to work with. Because reactivity is more explicit in Svelte 5, you can keep using the same API outside the top level of components. Head to [the tutorial](/tutorial) to learn more.
### $: → $derived/$effect
@ -120,7 +120,7 @@ In Svelte 5, the `$props` rune makes this straightforward without any additional
## Event changes
Event handlers have been given a facelift in Svelte 5. Whereas in Svelte 4 we use the `on:` directive to attach an event listener to an element, in Svelte 5 they are properties like any other (in other words - remove the colon):
Event handlers have been given a facelift in Svelte 5. Whereas in Svelte 4 we use the `on:` directive to attach an event listener to an element, in Svelte 5 they are properties like any other (in other words remove the colon):
```svelte
<script>
@ -154,7 +154,7 @@ Since they're just properties, you can use the normal shorthand syntax...
In Svelte 4, components could emit events by creating a dispatcher with `createEventDispatcher`.
This function is deprecated in Svelte 5. Instead, components should accept _callback props_ - which means you then pass functions as properties to these components:
This function is deprecated in Svelte 5. Instead, components should accept _callback props_ which means you then pass functions as properties to these components:
```svelte
<!--- file: App.svelte --->
@ -462,7 +462,7 @@ In Svelte 4, you would pass data to a `<slot />` and then retrieve it with `let:
## Migration script
By now you should have a pretty good understanding of the before/after and how the old syntax relates to the new syntax. It probably also became clear that a lot of these migrations are rather technical and repetitive - something you don't want to do by hand.
By now you should have a pretty good understanding of the before/after and how the old syntax relates to the new syntax. It probably also became clear that a lot of these migrations are rather technical and repetitive something you don't want to do by hand.
We thought the same, which is why we provide a migration script to do most of the migration automatically. You can upgrade your project by using `npx sv migrate svelte-5`. This will do the following things:

@ -18,7 +18,7 @@ There are online forums and chats which are a great place for discussion about b
## Are there any third-party resources?
Svelte Society maintains a [list of books and videos](https://sveltesociety.dev/resources).
Svelte Society maintains a [list of books and videos](https://sveltesociety.dev/collection/a-list-of-books-and-courses-ac01dd10363184fa).
## How can I get VS Code to syntax-highlight my .svelte files?
@ -65,11 +65,11 @@ There will be a blog post about this eventually, but in the meantime, check out
## Is there a UI component library?
There are several UI component libraries as well as standalone components. Find them under the [design systems section of the components page](https://sveltesociety.dev/packages?category=design-system) on the Svelte Society website.
There are several [UI component libraries](/packages#component-libraries) as well as standalone components listed on [the packages page](/packages).
## How do I test Svelte apps?
How your application is structured and where logic is defined will determine the best way to ensure it is properly tested. It is important to note that not all logic belongs within a component - this includes concerns such as data transformation, cross-component state management, and logging, among others. Remember that the Svelte library has its own test suite, so you do not need to write tests to validate implementation details provided by Svelte.
How your application is structured and where logic is defined will determine the best way to ensure it is properly tested. It is important to note that not all logic belongs within a component this includes concerns such as data transformation, cross-component state management, and logging, among others. Remember that the Svelte library has its own test suite, so you do not need to write tests to validate implementation details provided by Svelte.
A Svelte application will typically have three different types of tests: Unit, Component, and End-to-End (E2E).
@ -91,23 +91,15 @@ Some resources for getting started with testing:
## Is there a router?
The official routing library is [SvelteKit](/docs/kit). SvelteKit provides a filesystem router, server-side rendering (SSR), and hot module reloading (HMR) in one easy-to-use package. It shares similarities with Next.js for React.
The official routing library is [SvelteKit](/docs/kit). SvelteKit provides a filesystem router, server-side rendering (SSR), and hot module reloading (HMR) in one easy-to-use package. It shares similarities with Next.js for React and Nuxt.js for Vue. SvelteKit also supports hash-based routing for client-side applications.
However, you can use any router library. A lot of people use [page.js](https://github.com/visionmedia/page.js). There's also [navaid](https://github.com/lukeed/navaid), which is very similar. And [universal-router](https://github.com/kriasoft/universal-router), which is isomorphic with child routes, but without built-in history support.
If you prefer a declarative HTML approach, there's the isomorphic [svelte-routing](https://github.com/EmilTholin/svelte-routing) library and a fork of it called [svelte-navigator](https://github.com/mefechoel/svelte-navigator) containing some additional functionality.
If you need hash-based routing on the client side, check out the [hash option](https://svelte.dev/docs/kit/configuration#router) in SvelteKit, [svelte-spa-router](https://github.com/ItalyPaleAle/svelte-spa-router), or [abstract-state-router](https://github.com/TehShrike/abstract-state-router/).
[Routify](https://routify.dev) is another filesystem-based router, similar to SvelteKit's router. Version 3 supports Svelte's native SSR.
You can see a [community-maintained list of routers on sveltesociety.dev](https://sveltesociety.dev/packages?category=routers).
However, you can use any router library. A sampling of available routers are highlighted [on the packages page](/packages#routing).
## How do I write a mobile app with Svelte?
While most mobile apps are written without using JavaScript, if you'd like to leverage your existing Svelte components and knowledge of Svelte when building mobile apps, you can turn a [SvelteKit SPA](https://kit.svelte.dev/docs/single-page-apps) into a mobile app with [Tauri](https://v2.tauri.app/start/frontend/sveltekit/) or [Capacitor](https://capacitorjs.com/solution/svelte). Mobile features like the camera, geolocation, and push notifications are available via plugins for both platforms.
Svelte Native was an option available for Svelte 4, but note that Svelte 5 does not currently support it. Svelte Native lets you write NativeScript apps using Svelte components that contain [NativeScript UI components](https://docs.nativescript.org/ui/) rather than DOM elements, which may be familiar for users coming from React Native.
Some work has been completed towards [custom renderer support in Svelte 5](https://github.com/sveltejs/svelte/issues/15470), but this feature is not yet available. The custom rendering API would support additional mobile frameworks like Lynx JS and Svelte Native. Svelte Native was an option available for Svelte 4, but Svelte 5 does not currently support it. Svelte Native lets you write NativeScript apps using Svelte components that contain [NativeScript UI components](https://docs.nativescript.org/ui/) rather than DOM elements, which may be familiar for users coming from React Native.
## Can I tell Svelte not to remove my unused styles?

@ -140,12 +140,43 @@ The `flushSync()` function can be used to flush any pending effects synchronousl
This restriction only applies when using the `experimental.async` option, which will be active by default in Svelte 6.
### fork_discarded
```
Cannot commit a fork that was already discarded
```
### fork_timing
```
Cannot create a fork inside an effect or when state changes are pending
```
### get_abort_signal_outside_reaction
```
`getAbortSignal()` can only be called inside an effect or derived
```
### hydratable_missing_but_required
```
Expected to find a hydratable with key `%key%` during hydration, but did not.
```
This can happen if you render a hydratable on the client that was not rendered on the server, and means that it was forced to fall back to running its function blockingly during hydration. This is bad for performance, as it blocks hydration until the asynchronous work completes.
```svelte
<script>
import { hydratable } from 'svelte';
if (BROWSER) {
// bad! nothing can become interactive until this asynchronous work is done
await hydratable('foo', get_slow_random_number);
}
</script>
```
### hydration_failed
```

@ -140,6 +140,25 @@ The easiest way to log a value as it changes over time is to use the [`$inspect`
%handler% should be a function. Did you mean to %suggestion%?
```
### hydratable_missing_but_expected
```
Expected to find a hydratable with key `%key%` during hydration, but did not.
```
This can happen if you render a hydratable on the client that was not rendered on the server, and means that it was forced to fall back to running its function blockingly during hydration. This is bad for performance, as it blocks hydration until the asynchronous work completes.
```svelte
<script>
import { hydratable } from 'svelte';
if (BROWSER) {
// bad! nothing can become interactive until this asynchronous work is done
await hydratable('foo', get_slow_random_number);
}
</script>
```
### hydration_attribute_changed
```
@ -218,7 +237,7 @@ Hydration failed because the initial UI does not match what was rendered on the
This warning is thrown when Svelte encounters an error while hydrating the HTML from the server. During hydration, Svelte walks the DOM, expecting a certain structure. If that structure is different (for example because the HTML was repaired by the DOM because of invalid HTML), then Svelte will run into issues, resulting in this warning.
During development, this error is often preceeded by a `console.error` detailing the offending HTML, which needs fixing.
During development, this error is often preceded by a `console.error` detailing the offending HTML, which needs fixing.
### invalid_raw_snippet_render
@ -312,6 +331,27 @@ Reactive `$state(...)` proxies and the values they proxy have different identiti
To resolve this, ensure you're comparing values where both values were created with `$state(...)`, or neither were. Note that `$state.raw(...)` will _not_ create a state proxy.
### state_proxy_unmount
```
Tried to unmount a state proxy, rather than a component
```
`unmount` was called with a state proxy:
```js
import { mount, unmount } from 'svelte';
import Component from './Component.svelte';
let target = document.body;
// ---cut---
let component = $state(mount(Component, { target }));
// later...
unmount(component);
```
Avoid using `$state` here. If `component` _does_ need to be reactive for some reason, use `$state.raw` instead.
### svelte_boundary_reset_noop
```

@ -193,13 +193,13 @@ Cyclical dependency detected: %cycle%
### const_tag_invalid_placement
```
`{@const}` must be the immediate child of `{#snippet}`, `{#if}`, `{:else if}`, `{:else}`, `{#each}`, `{:then}`, `{:catch}`, `<svelte:fragment>`, `<svelte:boundary` or `<Component>`
`{@const}` must be the immediate child of `{#snippet}`, `{#if}`, `{:else if}`, `{:else}`, `{#each}`, `{:then}`, `{:catch}`, `<svelte:fragment>`, `<svelte:boundary>` or `<Component>`
```
### const_tag_invalid_reference
```
The `{@const %name% = ...}` declaration is not available in this snippet
The `{@const %name% = ...}` declaration is not available in this snippet
```
The following is an error:
@ -453,6 +453,12 @@ This turned out to be buggy and unpredictable, particularly when working with de
{/each}
```
### each_key_without_as
```
An `{#each ...}` block without an `as` clause cannot have a key
```
### effect_invalid_placement
```
@ -519,6 +525,12 @@ Expected an identifier
Expected identifier or destructure pattern
```
### expected_tag
```
Expected 'html', 'render', 'attach', 'const', or 'debug'
```
### expected_token
```
@ -555,6 +567,12 @@ Cannot use `await` in deriveds and template expressions, or at the top level of
`$host()` can only be used inside custom element component instances
```
### illegal_await_expression
```
`use:`, `transition:` and `animate:` directives, attachments and bindings do not support await expressions
```
### illegal_element_attribute
```
@ -1084,7 +1102,7 @@ Value must be %list%, if specified
### svelte_options_invalid_customelement
```
"customElement" must be a string literal defining a valid custom element name or an object of the form { tag?: string; shadow?: "open" | "none"; props?: { [key: string]: { attribute?: string; reflect?: boolean; type: .. } } }
"customElement" must be a string literal defining a valid custom element name or an object of the form { tag?: string; shadow?: "open" | "none" | `ShadowRootInit`; props?: { [key: string]: { attribute?: string; reflect?: boolean; type: .. } } }
```
### svelte_options_invalid_customelement_props
@ -1096,9 +1114,11 @@ Value must be %list%, if specified
### svelte_options_invalid_customelement_shadow
```
"shadow" must be either "open" or "none"
"shadow" must be either "open", "none" or `ShadowRootInit` object.
```
See https://developer.mozilla.org/en-US/docs/Web/API/Element/attachShadow#options for more information on valid shadow root constructor options
### svelte_options_invalid_tagname
```

@ -81,7 +81,7 @@ Coding for the keyboard is important for users with physical disabilities who ca
### a11y_consider_explicit_label
```
Buttons and links should either contain text or have an `aria-label` or `aria-labelledby` attribute
Buttons and links should either contain text or have an `aria-label`, `aria-labelledby` or `title` attribute
```
### a11y_distracting_elements

@ -1,5 +1,66 @@
<!-- This file is generated by scripts/process-messages/index.js. Do not edit! -->
### async_local_storage_unavailable
```
The node API `AsyncLocalStorage` is not available, but is required to use async server rendering.
```
Some platforms require configuration flags to enable this API. Consult your platform's documentation.
### await_invalid
```
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.
### html_deprecated
```
The `html` property of server render results has been deprecated. Use `body` instead.
```
### hydratable_clobbering
```
Attempted to set `hydratable` with key `%key%` twice with different values.
%stack%
```
This error occurs when using `hydratable` multiple times with the same key. To avoid this, you can:
- Ensure all invocations with the same key result in the same value
- Update the keys to make both instances unique
```svelte
<script>
import { hydratable } from 'svelte';
// which one should "win" and be serialized in the rendered response?
const one = hydratable('not-unique', () => 1);
const two = hydratable('not-unique', () => 2);
</script>
```
### hydratable_serialization_failed
```
Failed to serialize `hydratable` data for key `%key%`.
`hydratable` can serialize anything [`uneval` from `devalue`](https://npmjs.com/package/uneval) can, plus Promises.
Cause:
%stack%
```
### invalid_csp
```
`csp.nonce` was set while `csp.hash` was `true`. These options cannot be used simultaneously.
```
### lifecycle_function_unavailable
```
@ -7,3 +68,11 @@
```
Certain methods such as `mount` cannot be invoked while running in a server context. Avoid calling them eagerly, i.e. not during render.
### server_context_required
```
Could not resolve `render` context.
```
Certain functions such as `hydratable` cannot be invoked outside of a `render(...)` call, such as at the top level of a module.

@ -0,0 +1,34 @@
<!-- This file is generated by scripts/process-messages/index.js. Do not edit! -->
### unresolved_hydratable
```
A `hydratable` value with key `%key%` was created, but at least part of it was not used during the render.
The `hydratable` was initialized in:
%stack%
```
The most likely cause of this is creating a `hydratable` in the `script` block of your component and then `await`ing
the result inside a `svelte:boundary` with a `pending` snippet:
```svelte
<script>
import { hydratable } from 'svelte';
import { getUser } from '$lib/get-user.js';
const user = hydratable('user', getUser);
</script>
<svelte:boundary>
<h1>{(await user).name}</h1>
{#snippet pending()}
<div>Loading...</div>
{/snippet}
</svelte:boundary>
```
Consider inlining the `hydratable` call inside the boundary so that it's not called on the server.
Note that this can also happen when a `hydratable` contains multiple promises and some but not all of them have been used.

@ -1,25 +1,11 @@
<!-- This file is generated by scripts/process-messages/index.js. Do not edit! -->
### await_outside_boundary
### experimental_async_required
```
Cannot await outside a `<svelte:boundary>` with a `pending` snippet
Cannot use `%name%(...)` unless the `experimental.async` compiler option is `true`
```
The `await` keyword can only appear in a `$derived(...)` or template expression, or at the top level of a component's `<script>` block, if it is inside a [`<svelte:boundary>`](/docs/svelte/svelte-boundary) that has a `pending` snippet:
```svelte
<svelte:boundary>
<p>{await getData()}</p>
{#snippet pending()}
<p>loading...</p>
{/snippet}
</svelte:boundary>
```
This restriction may be lifted in a future version of Svelte.
### invalid_default_snippet
```
@ -80,6 +66,14 @@ Certain lifecycle methods can only be used during component initialisation. To f
<button onclick={handleClick}>click me</button>
```
### missing_context
```
Context was not set in a parent component
```
The [`createContext()`](svelte#createContext) utility returns a `[get, set]` pair of functions. `get` will throw an error if `set` was not used to set the context in a parent component.
### snippet_without_render_tag
```

@ -43,7 +43,7 @@ The following modifiers are available:
- `preventDefault` — calls `event.preventDefault()` before running the handler
- `stopPropagation` — calls `event.stopPropagation()`, preventing the event reaching the next element
- `stopImmediatePropagation` - calls `event.stopImmediatePropagation()`, preventing other listeners of the same event from being fired.
- `stopImmediatePropagation` calls `event.stopImmediatePropagation()`, preventing other listeners of the same event from being fired.
- `passive` — improves scrolling performance on touch/wheel events (Svelte will add it automatically where it's safe to do so)
- `nonpassive` — explicitly set `passive: false`
- `capture` — fires the handler during the _capture_ phase instead of the _bubbling_ phase

@ -94,6 +94,7 @@ export default [
'packages/svelte/src/internal/client/errors.js',
'packages/svelte/src/internal/client/warnings.js',
'packages/svelte/src/internal/shared/warnings.js',
'packages/svelte/src/internal/server/warnings.js',
'packages/svelte/compiler/index.js',
// stuff we don't want to lint
'benchmarking/**',

@ -21,15 +21,16 @@
"test": "vitest run",
"changeset:version": "changeset version && pnpm -r generate:version && git add --all",
"changeset:publish": "changeset publish",
"bench": "node --allow-natives-syntax ./benchmarking/run.js",
"bench:compare": "node --allow-natives-syntax ./benchmarking/compare/index.js",
"bench:debug": "node --allow-natives-syntax --inspect-brk ./benchmarking/run.js"
"bench": "NODE_ENV=production node --allow-natives-syntax ./benchmarking/run.js",
"bench:compare": "NODE_ENV=production node --allow-natives-syntax ./benchmarking/compare/index.js",
"bench:debug": "NODE_ENV=production node --allow-natives-syntax --inspect-brk ./benchmarking/run.js"
},
"devDependencies": {
"@changesets/cli": "^2.27.8",
"@changesets/cli": "^2.29.8",
"@sveltejs/eslint-config": "^8.3.3",
"@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": "^9.9.1",
"eslint-plugin-lube": "^0.4.3",
@ -40,7 +41,7 @@
"prettier-plugin-svelte": "^3.4.0",
"svelte": "workspace:^",
"typescript": "^5.5.4",
"typescript-eslint": "^8.24.0",
"typescript-eslint": "^8.48.1",
"v8-natives": "^1.2.5",
"vitest": "^2.1.9"
}

@ -1,5 +1,673 @@
# svelte
## 5.49.0
### Minor Changes
- feat: allow passing `ShadowRootInit` object to custom element `shadow` option ([#17088](https://github.com/sveltejs/svelte/pull/17088))
### Patch Changes
- fix: throw for unset `createContext` get on the server ([#17580](https://github.com/sveltejs/svelte/pull/17580))
- fix: reset effects inside skipped branches ([#17581](https://github.com/sveltejs/svelte/pull/17581))
- fix: preserve old dependencies when updating reaction inside fork ([#17579](https://github.com/sveltejs/svelte/pull/17579))
- fix: more conservative assignment_value_stale warnings ([#17574](https://github.com/sveltejs/svelte/pull/17574))
- fix: disregard `popover` elements when determining whether an element has content ([#17367](https://github.com/sveltejs/svelte/pull/17367))
- fix: fire introstart/outrostart events after delay, if specified ([#17567](https://github.com/sveltejs/svelte/pull/17567))
- fix: increment signal versions when discarding forks ([#17577](https://github.com/sveltejs/svelte/pull/17577))
## 5.48.5
### Patch Changes
- fix: run boundary `onerror` callbacks in a microtask, in case they result in the boundary's destruction ([#17561](https://github.com/sveltejs/svelte/pull/17561))
- fix: prevent unintended exports from namespaces ([#17562](https://github.com/sveltejs/svelte/pull/17562))
- fix: each block breaking with effects interspersed among items ([#17550](https://github.com/sveltejs/svelte/pull/17550))
## 5.48.4
### Patch Changes
- fix: avoid duplicating escaped characters in CSS AST ([#17554](https://github.com/sveltejs/svelte/pull/17554))
## 5.48.3
### Patch Changes
- fix: hydration failing with settled async blocks ([#17539](https://github.com/sveltejs/svelte/pull/17539))
- fix: add pointer and touch events to a11y_no_static_element_interactions warning ([#17551](https://github.com/sveltejs/svelte/pull/17551))
- fix: handle false dynamic components in SSR ([#17542](https://github.com/sveltejs/svelte/pull/17542))
- fix: avoid unnecessary block effect re-runs after async work completes ([#17535](https://github.com/sveltejs/svelte/pull/17535))
- fix: avoid using dev-mode array.includes wrapper on internal array checks ([#17536](https://github.com/sveltejs/svelte/pull/17536))
## 5.48.2
### Patch Changes
- fix: export `wait` function from internal client index ([#17530](https://github.com/sveltejs/svelte/pull/17530))
## 5.48.1
### Patch Changes
- fix: hoist snippets above const in same block ([#17516](https://github.com/sveltejs/svelte/pull/17516))
- fix: properly hydrate await in `{@html}` ([#17528](https://github.com/sveltejs/svelte/pull/17528))
- fix: batch resolution of async work ([#17511](https://github.com/sveltejs/svelte/pull/17511))
- fix: account for empty statements when visiting in transform async ([#17524](https://github.com/sveltejs/svelte/pull/17524))
- fix: avoid async overhead for already settled promises ([#17461](https://github.com/sveltejs/svelte/pull/17461))
- fix: better code generation for const tags with async dependencies ([#17518](https://github.com/sveltejs/svelte/pull/17518))
## 5.48.0
### Minor Changes
- feat: export `parseCss` from `svelte/compiler` ([#17496](https://github.com/sveltejs/svelte/pull/17496))
### Patch Changes
- fix: handle non-string values in `svelte:element` `this` attribute ([#17499](https://github.com/sveltejs/svelte/pull/17499))
- fix: faster deduplication of dependencies ([#17503](https://github.com/sveltejs/svelte/pull/17503))
## 5.47.1
### Patch Changes
- fix: trigger `selectedcontent` reactivity ([#17486](https://github.com/sveltejs/svelte/pull/17486))
## 5.47.0
### Minor Changes
- feat: customizable `<select>` elements ([#17429](https://github.com/sveltejs/svelte/pull/17429))
### Patch Changes
- fix: mark subtree of svelte boundary as dynamic ([#17468](https://github.com/sveltejs/svelte/pull/17468))
- fix: don't reset static elements with debug/snippets ([#17477](https://github.com/sveltejs/svelte/pull/17477))
## 5.46.4
### Patch Changes
- fix: use `devalue.uneval` to serialize `hydratable` keys ([`ef81048e238844b729942441541d6dcfe6c8ccca`](https://github.com/sveltejs/svelte/commit/ef81048e238844b729942441541d6dcfe6c8ccca))
## 5.46.3
### Patch Changes
- fix: reconnect clean deriveds when they are read in a reactive context ([#17362](https://github.com/sveltejs/svelte/pull/17362))
- fix: don't transform references of function declarations in legacy mode ([#17431](https://github.com/sveltejs/svelte/pull/17431))
- fix: notify deriveds of changes to sources inside forks ([#17437](https://github.com/sveltejs/svelte/pull/17437))
- fix: always reconnect deriveds in get, when appropriate ([#17451](https://github.com/sveltejs/svelte/pull/17451))
- fix: prevent derives without dependencies from ever re-running ([`286b40c4526ce9970cb81ddd5e65b93b722fe468`](https://github.com/sveltejs/svelte/commit/286b40c4526ce9970cb81ddd5e65b93b722fe468))
- fix: correctly update writable deriveds inside forks ([#17437](https://github.com/sveltejs/svelte/pull/17437))
- fix: remove `$inspect` calls after await expressions when compiling for production server code ([#17407](https://github.com/sveltejs/svelte/pull/17407))
- fix: clear batch between runs ([#17424](https://github.com/sveltejs/svelte/pull/17424))
- fix: adjust `loc` property of `Program` nodes created from `<script>` elements ([#17428](https://github.com/sveltejs/svelte/pull/17428))
- fix: don't revert source to UNINITIALIZED state when time travelling ([#17409](https://github.com/sveltejs/svelte/pull/17409))
## 5.46.2
### Notice
Not published due to CI issue
## 5.46.1
### Patch Changes
- fix: type `currentTarget` in `on` function ([#17370](https://github.com/sveltejs/svelte/pull/17370))
- fix: skip static optimisation for stateless deriveds after `await` ([#17389](https://github.com/sveltejs/svelte/pull/17389))
- fix: prevent infinite loop when HMRing a component with an `await` ([#17380](https://github.com/sveltejs/svelte/pull/17380))
## 5.46.0
### Minor Changes
- feat: Add `csp` option to `render(...)`, and emit hashes when using `hydratable` ([#17338](https://github.com/sveltejs/svelte/pull/17338))
## 5.45.10
### Patch Changes
- fix: race condition when importing `AsyncLocalStorage` ([#17350](https://github.com/sveltejs/svelte/pull/17350))
## 5.45.9
### Patch Changes
- fix: correctly reschedule deferred effects when reviving a batch after async work ([#17332](https://github.com/sveltejs/svelte/pull/17332))
- fix: correctly print `!doctype` during `print` ([#17341](https://github.com/sveltejs/svelte/pull/17341))
## 5.45.8
### Patch Changes
- fix: set AST `root.start` to `0` and `root.end` to `template.length` ([#17125](https://github.com/sveltejs/svelte/pull/17125))
- fix: prevent erroneous `state_referenced_locally` warnings on prop fallbacks ([#17329](https://github.com/sveltejs/svelte/pull/17329))
## 5.45.7
### Patch Changes
- fix: Add `<textarea wrap="off">` as a valid attribute value ([#17326](https://github.com/sveltejs/svelte/pull/17326))
- fix: add more css selectors to `print()` ([#17330](https://github.com/sveltejs/svelte/pull/17330))
- fix: don't crash on `hydratable` serialization failure ([#17315](https://github.com/sveltejs/svelte/pull/17315))
## 5.45.6
### Patch Changes
- fix: don't issue a11y warning for `<video>` without captions if it has no `src` ([#17311](https://github.com/sveltejs/svelte/pull/17311))
- fix: add `srcObject` to permitted `<audio>`/`<video>` attributes ([#17310](https://github.com/sveltejs/svelte/pull/17310))
## 5.45.5
### Patch Changes
- fix: correctly reconcile each blocks after outroing branches are resumed ([#17258](https://github.com/sveltejs/svelte/pull/17258))
- fix: destroy each items after siblings are resumed ([#17258](https://github.com/sveltejs/svelte/pull/17258))
## 5.45.4
### Patch Changes
- chore: move DOM-related effect properties to `effect.nodes` ([#17293](https://github.com/sveltejs/svelte/pull/17293))
- fix: allow `$props.id()` to occur after an `await` ([#17285](https://github.com/sveltejs/svelte/pull/17285))
- fix: keep reactions up to date even when read outside of effect ([#17295](https://github.com/sveltejs/svelte/pull/17295))
## 5.45.3
### Patch Changes
- add props to state_referenced_locally ([#17266](https://github.com/sveltejs/svelte/pull/17266))
- fix: preserve node locations for better sourcemaps ([#17269](https://github.com/sveltejs/svelte/pull/17269))
- fix: handle cross-realm Promises in `hydratable` ([#17284](https://github.com/sveltejs/svelte/pull/17284))
## 5.45.2
### Patch Changes
- fix: array destructuring after await ([#17254](https://github.com/sveltejs/svelte/pull/17254))
- fix: throw on invalid `{@tag}`s ([#17256](https://github.com/sveltejs/svelte/pull/17256))
## 5.45.1
### Patch Changes
- fix: link offscreen items and last effect in each block correctly ([#17240](https://github.com/sveltejs/svelte/pull/17240))
## 5.45.0
### Minor Changes
- feat: add `print(...)` function ([#16188](https://github.com/sveltejs/svelte/pull/16188))
## 5.44.1
### Patch Changes
- fix: await blockers before initialising const ([#17226](https://github.com/sveltejs/svelte/pull/17226))
- fix: link offscreen items and last effect in each block correctly ([#17244](https://github.com/sveltejs/svelte/pull/17244))
- fix: generate correct code for simple destructurings ([#17237](https://github.com/sveltejs/svelte/pull/17237))
- fix: ensure each block animations don't mess with transitions ([#17238](https://github.com/sveltejs/svelte/pull/17238))
## 5.44.0
### Minor Changes
- feat: `hydratable` API ([#17154](https://github.com/sveltejs/svelte/pull/17154))
## 5.43.15
### Patch Changes
- fix: don't execute attachments and attribute effects eagerly ([#17208](https://github.com/sveltejs/svelte/pull/17208))
- chore: lift "flushSync cannot be called in effects" restriction ([#17139](https://github.com/sveltejs/svelte/pull/17139))
- fix: store forked derived values ([#17212](https://github.com/sveltejs/svelte/pull/17212))
## 5.43.14
### Patch Changes
- fix: correctly migrate named self closing slots ([#17199](https://github.com/sveltejs/svelte/pull/17199))
- fix: error at compile time instead of at runtime on await expressions inside bindings/transitions/animations/attachments ([#17198](https://github.com/sveltejs/svelte/pull/17198))
- fix: take async blockers into account for bindings/transitions/animations/attachments ([#17198](https://github.com/sveltejs/svelte/pull/17198))
## 5.43.13
### Patch Changes
- fix: don't set derived values during time traveling ([#17200](https://github.com/sveltejs/svelte/pull/17200))
## 5.43.12
### Patch Changes
- fix: maintain correct linked list of effects when updating each blocks ([#17191](https://github.com/sveltejs/svelte/pull/17191))
## 5.43.11
### Patch Changes
- perf: don't use tracing overeager during dev ([#17183](https://github.com/sveltejs/svelte/pull/17183))
- fix: don't cancel transition of already outroing elements ([#17186](https://github.com/sveltejs/svelte/pull/17186))
## 5.43.10
### Patch Changes
- fix: avoid other batches running with queued root effects of main batch ([#17145](https://github.com/sveltejs/svelte/pull/17145))
## 5.43.9
### Patch Changes
- fix: correctly handle functions when determining async blockers ([#17137](https://github.com/sveltejs/svelte/pull/17137))
- fix: keep deriveds reactive after their original parent effect was destroyed ([#17171](https://github.com/sveltejs/svelte/pull/17171))
- fix: ensure eager effects don't break reactions chain ([#17138](https://github.com/sveltejs/svelte/pull/17138))
- fix: ensure async `@const` in boundary hydrates correctly ([#17165](https://github.com/sveltejs/svelte/pull/17165))
- fix: take blockers into account when creating `#await` blocks ([#17137](https://github.com/sveltejs/svelte/pull/17137))
- fix: parallelize async `@const`s in the template ([#17165](https://github.com/sveltejs/svelte/pull/17165))
## 5.43.8
### Patch Changes
- fix: each block losing reactivity when items removed while promise pending ([#17150](https://github.com/sveltejs/svelte/pull/17150))
## 5.43.7
### Patch Changes
- fix: properly defer document title until async work is complete ([#17158](https://github.com/sveltejs/svelte/pull/17158))
- fix: ensure deferred effects can be rescheduled later on ([#17147](https://github.com/sveltejs/svelte/pull/17147))
- fix: take blockers of components into account ([#17153](https://github.com/sveltejs/svelte/pull/17153))
## 5.43.6
### Patch Changes
- fix: don't deactivate other batches ([#17132](https://github.com/sveltejs/svelte/pull/17132))
## 5.43.5
### Patch Changes
- fix: ensure async static props/attributes are awaited ([#17120](https://github.com/sveltejs/svelte/pull/17120))
- fix: wait on dependencies of async bindings ([#17120](https://github.com/sveltejs/svelte/pull/17120))
- fix: await dependencies of style directives ([#17120](https://github.com/sveltejs/svelte/pull/17120))
## 5.43.4
### Patch Changes
- chore: simplify connection/disconnection logic ([#17105](https://github.com/sveltejs/svelte/pull/17105))
- fix: reconnect deriveds to effect tree when time-travelling ([#17105](https://github.com/sveltejs/svelte/pull/17105))
## 5.43.3
### Patch Changes
- fix: ensure fork always accesses correct values ([#17098](https://github.com/sveltejs/svelte/pull/17098))
- fix: change title only after any pending work has completed ([#17061](https://github.com/sveltejs/svelte/pull/17061))
- fix: preserve symbols when creating derived rest properties ([#17096](https://github.com/sveltejs/svelte/pull/17096))
## 5.43.2
### Patch Changes
- fix: treat each blocks with async dependencies as uncontrolled ([#17077](https://github.com/sveltejs/svelte/pull/17077))
## 5.43.1
### Patch Changes
- fix: transform `$bindable` after `await` expressions ([#17066](https://github.com/sveltejs/svelte/pull/17066))
## 5.43.0
### Minor Changes
- feat: out-of-order rendering ([#17038](https://github.com/sveltejs/svelte/pull/17038))
### Patch Changes
- fix: settle batch after DOM updates ([#17054](https://github.com/sveltejs/svelte/pull/17054))
## 5.42.3
### Patch Changes
- fix: handle `<svelte:head>` rendered asynchronously ([#17052](https://github.com/sveltejs/svelte/pull/17052))
- fix: don't restore batch in `#await` ([#17051](https://github.com/sveltejs/svelte/pull/17051))
## 5.42.2
### Patch Changes
- fix: better error message for global variable assignments ([#17036](https://github.com/sveltejs/svelte/pull/17036))
- chore: tweak memoizer logic ([#17042](https://github.com/sveltejs/svelte/pull/17042))
## 5.42.1
### Patch Changes
- fix: ignore fork `discard()` after `commit()` ([#17034](https://github.com/sveltejs/svelte/pull/17034))
## 5.42.0
### Minor Changes
- feat: experimental `fork` API ([#17004](https://github.com/sveltejs/svelte/pull/17004))
### Patch Changes
- fix: always allow `setContext` before first await in component ([#17031](https://github.com/sveltejs/svelte/pull/17031))
- fix: less confusing names for inspect errors ([#17026](https://github.com/sveltejs/svelte/pull/17026))
## 5.41.4
### Patch Changes
- fix: take into account static blocks when determining transition locality ([#17018](https://github.com/sveltejs/svelte/pull/17018))
- fix: coordinate mount of snippets with await expressions ([#17021](https://github.com/sveltejs/svelte/pull/17021))
- fix: better optimization of await expressions ([#17025](https://github.com/sveltejs/svelte/pull/17025))
- fix: flush pending changes after rendering `failed` snippet ([#16995](https://github.com/sveltejs/svelte/pull/16995))
## 5.41.3
### Patch Changes
- chore: exclude vite optimized deps from stack traces ([#17008](https://github.com/sveltejs/svelte/pull/17008))
- perf: skip repeatedly traversing the same derived ([#17016](https://github.com/sveltejs/svelte/pull/17016))
## 5.41.2
### Patch Changes
- fix: keep batches alive until all async work is complete ([#16971](https://github.com/sveltejs/svelte/pull/16971))
- fix: don't preserve reactivity context across function boundaries ([#17002](https://github.com/sveltejs/svelte/pull/17002))
- fix: make `$inspect` logs come from the callsite ([#17001](https://github.com/sveltejs/svelte/pull/17001))
- fix: ensure guards (eg. if, each, key) run before their contents ([#16930](https://github.com/sveltejs/svelte/pull/16930))
## 5.41.1
### Patch Changes
- fix: place `let:` declarations before `{@const}` declarations ([#16985](https://github.com/sveltejs/svelte/pull/16985))
- fix: improve `each_key_without_as` error ([#16983](https://github.com/sveltejs/svelte/pull/16983))
- chore: centralise branch management ([#16977](https://github.com/sveltejs/svelte/pull/16977))
## 5.41.0
### Minor Changes
- feat: add `$state.eager(value)` rune ([#16849](https://github.com/sveltejs/svelte/pull/16849))
### Patch Changes
- fix: preserve `<select>` state while focused ([#16958](https://github.com/sveltejs/svelte/pull/16958))
- chore: run boundary async effects in the context of the current batch ([#16968](https://github.com/sveltejs/svelte/pull/16968))
- fix: error if `each` block has `key` but no `as` clause ([#16966](https://github.com/sveltejs/svelte/pull/16966))
## 5.40.2
### Patch Changes
- fix: add hydration markers in `pending` branch of SSR boundary ([#16965](https://github.com/sveltejs/svelte/pull/16965))
## 5.40.1
### Patch Changes
- chore: Remove sync-in-async warning for server rendering ([#16949](https://github.com/sveltejs/svelte/pull/16949))
## 5.40.0
### Minor Changes
- feat: add `createContext` utility for type-safe context ([#16948](https://github.com/sveltejs/svelte/pull/16948))
### Patch Changes
- chore: simplify `batch.apply()` ([#16945](https://github.com/sveltejs/svelte/pull/16945))
- fix: don't rerun async effects unnecessarily ([#16944](https://github.com/sveltejs/svelte/pull/16944))
## 5.39.13
### Patch Changes
- fix: add missing type for `fr` attribute for `radialGradient` tags in svg ([#16943](https://github.com/sveltejs/svelte/pull/16943))
- fix: unset context on stale promises ([#16935](https://github.com/sveltejs/svelte/pull/16935))
## 5.39.12
### Patch Changes
- fix: better input cursor restoration for `bind:value` ([#16925](https://github.com/sveltejs/svelte/pull/16925))
- fix: track the user's getter of `bind:this` ([#16916](https://github.com/sveltejs/svelte/pull/16916))
- fix: generate correct SSR code for the case where `pending` is an attribute ([#16919](https://github.com/sveltejs/svelte/pull/16919))
- fix: generate correct code for `each` blocks with async body ([#16923](https://github.com/sveltejs/svelte/pull/16923))
## 5.39.11
### Patch Changes
- fix: flush batches whenever an async value resolves ([#16912](https://github.com/sveltejs/svelte/pull/16912))
## 5.39.10
### Patch Changes
- fix: hydrate each blocks inside element correctly ([#16908](https://github.com/sveltejs/svelte/pull/16908))
- fix: allow await in if block consequent and alternate ([#16890](https://github.com/sveltejs/svelte/pull/16890))
- fix: don't replace rest props with `$$props` for excluded props ([#16898](https://github.com/sveltejs/svelte/pull/16898))
- fix: correctly transform `$derived` private fields on server ([#16894](https://github.com/sveltejs/svelte/pull/16894))
- fix: add `UNKNOWN` evaluation value before breaking for `binding.initial===SnippetBlock` ([#16910](https://github.com/sveltejs/svelte/pull/16910))
## 5.39.9
### Patch Changes
- fix: flush when pending boundaries resolve ([#16897](https://github.com/sveltejs/svelte/pull/16897))
## 5.39.8
### Patch Changes
- fix: check boundary `pending` attribute at runtime on server ([#16855](https://github.com/sveltejs/svelte/pull/16855))
- fix: preserve tuple type in `$state.snapshot` ([#16864](https://github.com/sveltejs/svelte/pull/16864))
- fix: allow await in svelte:boundary without pending ([#16857](https://github.com/sveltejs/svelte/pull/16857))
- fix: update `bind:checked` error message to clarify usage with radio inputs ([#16874](https://github.com/sveltejs/svelte/pull/16874))
## 5.39.7
### Patch Changes
- chore: simplify batch logic ([#16847](https://github.com/sveltejs/svelte/pull/16847))
- fix: rebase pending batches when other batches are committed ([#16866](https://github.com/sveltejs/svelte/pull/16866))
- fix: wrap async `children` in `$$renderer.async` ([#16862](https://github.com/sveltejs/svelte/pull/16862))
- fix: silence label warning for buttons and anchor tags with title attributes ([#16872](https://github.com/sveltejs/svelte/pull/16872))
- fix: coerce nullish `<title>` to empty string ([#16863](https://github.com/sveltejs/svelte/pull/16863))
## 5.39.6
### Patch Changes
- fix: depend on reads of deriveds created within reaction (async mode) ([#16823](https://github.com/sveltejs/svelte/pull/16823))
- fix: SSR regression of processing attributes of `<select>` and `<option>` ([#16821](https://github.com/sveltejs/svelte/pull/16821))
- fix: async `class:` + spread attributes were compiled into sync server-side code ([#16834](https://github.com/sveltejs/svelte/pull/16834))
- fix: ensure tick resolves within a macrotask ([#16825](https://github.com/sveltejs/svelte/pull/16825))
## 5.39.5
### Patch Changes
- fix: allow `{@html await ...}` and snippets with async content on the server ([#16817](https://github.com/sveltejs/svelte/pull/16817))
- fix: use nginx SSI-compatible comments for `$props.id()` ([#16820](https://github.com/sveltejs/svelte/pull/16820))
## 5.39.4
### Patch Changes
- fix: restore hydration state after `await` in `<script>` ([#16806](https://github.com/sveltejs/svelte/pull/16806))
## 5.39.3
### Patch Changes
- fix: remove outer hydration markers ([#16800](https://github.com/sveltejs/svelte/pull/16800))
- fix: async hydration ([#16797](https://github.com/sveltejs/svelte/pull/16797))
## 5.39.2
### Patch Changes
- fix: preserve SSR context when block expressions contain `await` ([#16791](https://github.com/sveltejs/svelte/pull/16791))
- chore: bump some devDependencies ([#16787](https://github.com/sveltejs/svelte/pull/16787))
## 5.39.1
### Patch Changes
- fix: issue `state_proxy_unmount` warning when unmounting a state proxy ([#16747](https://github.com/sveltejs/svelte/pull/16747))
- fix: add `then` to class component `render` output ([#16783](https://github.com/sveltejs/svelte/pull/16783))
## 5.39.0
### Minor Changes
- feat: experimental async SSR ([#16748](https://github.com/sveltejs/svelte/pull/16748))
### Patch Changes
- fix: correctly SSR hidden="until-found" ([#16773](https://github.com/sveltejs/svelte/pull/16773))
## 5.38.10
### Patch Changes
- fix: flush effects scheduled during boundary's pending phase ([#16738](https://github.com/sveltejs/svelte/pull/16738))
## 5.38.9
### Patch Changes
- chore: generate CSS hash using the filename ([#16740](https://github.com/sveltejs/svelte/pull/16740))
- fix: correctly analyze `<object.property>` components ([#16711](https://github.com/sveltejs/svelte/pull/16711))
- fix: clean up scheduling system ([#16741](https://github.com/sveltejs/svelte/pull/16741))
- fix: transform input defaults from spread ([#16481](https://github.com/sveltejs/svelte/pull/16481))
- fix: don't destroy contents of `svelte:boundary` unless the boundary is an error boundary ([#16746](https://github.com/sveltejs/svelte/pull/16746))
## 5.38.8
### Patch Changes
- fix: send `$effect.pending` count to the correct boundary ([#16732](https://github.com/sveltejs/svelte/pull/16732))
## 5.38.7
### Patch Changes

@ -1239,6 +1239,7 @@ export interface HTMLMediaAttributes<T extends HTMLMediaElement> extends HTMLAtt
playsinline?: boolean | undefined | null;
preload?: 'auto' | 'none' | 'metadata' | '' | undefined | null;
src?: string | undefined | null;
srcobject?: MediaStream | MediaSource | File | Blob;
/**
* a value between 0 and 1
*/
@ -1421,7 +1422,7 @@ export interface HTMLTextareaAttributes extends HTMLAttributes<HTMLTextAreaEleme
// needs both casing variants because language tools does lowercase names of non-shorthand attributes
defaultValue?: string | string[] | number | undefined | null;
defaultvalue?: string | string[] | number | undefined | null;
wrap?: 'hard' | 'soft' | undefined | null;
wrap?: 'hard' | 'soft' | 'off' | undefined | null;
'on:change'?: ChangeEventHandler<HTMLTextAreaElement> | undefined | null;
onchange?: ChangeEventHandler<HTMLTextAreaElement> | undefined | null;
@ -1658,6 +1659,7 @@ export interface SVGAttributes<T extends EventTarget> extends AriaAttributes, DO
'font-variant'?: number | string | undefined | null;
'font-weight'?: number | string | undefined | null;
format?: number | string | undefined | null;
fr?: number | string | undefined | null;
from?: number | string | undefined | null;
fx?: number | string | undefined | null;
fy?: number | string | undefined | null;
@ -2060,7 +2062,7 @@ export interface SvelteHTMLElements {
| undefined
| {
tag?: string;
shadow?: 'open' | 'none' | undefined;
shadow?: 'open' | 'none' | ShadowRootInit | undefined;
props?:
| Record<
string,

@ -108,10 +108,35 @@ The `flushSync()` function can be used to flush any pending effects synchronousl
This restriction only applies when using the `experimental.async` option, which will be active by default in Svelte 6.
## fork_discarded
> Cannot commit a fork that was already discarded
## fork_timing
> Cannot create a fork inside an effect or when state changes are pending
## get_abort_signal_outside_reaction
> `getAbortSignal()` can only be called inside an effect or derived
## hydratable_missing_but_required
> Expected to find a hydratable with key `%key%` during hydration, but did not.
This can happen if you render a hydratable on the client that was not rendered on the server, and means that it was forced to fall back to running its function blockingly during hydration. This is bad for performance, as it blocks hydration until the asynchronous work completes.
```svelte
<script>
import { hydratable } from 'svelte';
if (BROWSER) {
// bad! nothing can become interactive until this asynchronous work is done
await hydratable('foo', get_slow_random_number);
}
</script>
```
## hydration_failed
> Failed to hydrate the application

@ -124,6 +124,23 @@ The easiest way to log a value as it changes over time is to use the [`$inspect`
> %handler% should be a function. Did you mean to %suggestion%?
## hydratable_missing_but_expected
> Expected to find a hydratable with key `%key%` during hydration, but did not.
This can happen if you render a hydratable on the client that was not rendered on the server, and means that it was forced to fall back to running its function blockingly during hydration. This is bad for performance, as it blocks hydration until the asynchronous work completes.
```svelte
<script>
import { hydratable } from 'svelte';
if (BROWSER) {
// bad! nothing can become interactive until this asynchronous work is done
await hydratable('foo', get_slow_random_number);
}
</script>
```
## hydration_attribute_changed
> The `%attribute%` attribute on `%html%` changed its value between server and client renders. The client value, `%value%`, will be ignored in favour of the server value
@ -192,7 +209,7 @@ To fix this, either silence the warning with a [`svelte-ignore`](basic-markup#Co
This warning is thrown when Svelte encounters an error while hydrating the HTML from the server. During hydration, Svelte walks the DOM, expecting a certain structure. If that structure is different (for example because the HTML was repaired by the DOM because of invalid HTML), then Svelte will run into issues, resulting in this warning.
During development, this error is often preceeded by a `console.error` detailing the offending HTML, which needs fixing.
During development, this error is often preceded by a `console.error` detailing the offending HTML, which needs fixing.
## invalid_raw_snippet_render
@ -272,6 +289,25 @@ To silence the warning, ensure that `value`:
To resolve this, ensure you're comparing values where both values were created with `$state(...)`, or neither were. Note that `$state.raw(...)` will _not_ create a state proxy.
## state_proxy_unmount
> Tried to unmount a state proxy, rather than a component
`unmount` was called with a state proxy:
```js
import { mount, unmount } from 'svelte';
import Component from './Component.svelte';
let target = document.body;
// ---cut---
let component = $state(mount(Component, { target }));
// later...
unmount(component);
```
Avoid using `$state` here. If `component` _does_ need to be reactive for some reason, use `$state.raw` instead.
## svelte_boundary_reset_noop
> A `<svelte:boundary>` `reset` function only resets the boundary the first time it is called

@ -122,11 +122,11 @@
## const_tag_invalid_placement
> `{@const}` must be the immediate child of `{#snippet}`, `{#if}`, `{:else if}`, `{:else}`, `{#each}`, `{:then}`, `{:catch}`, `<svelte:fragment>`, `<svelte:boundary` or `<Component>`
> `{@const}` must be the immediate child of `{#snippet}`, `{#if}`, `{:else if}`, `{:else}`, `{#each}`, `{:then}`, `{:catch}`, `<svelte:fragment>`, `<svelte:boundary>` or `<Component>`
## const_tag_invalid_reference
> The `{@const %name% = ...}` declaration is not available in this snippet
> The `{@const %name% = ...}` declaration is not available in this snippet
The following is an error:
@ -179,6 +179,10 @@ The same applies to components:
> `%type%` name cannot be empty
## each_key_without_as
> An `{#each ...}` block without an `as` clause cannot have a key
## element_invalid_closing_tag
> `</%name%>` attempted to close an element that was not open
@ -219,6 +223,10 @@ The same applies to components:
> Expected identifier or destructure pattern
## expected_tag
> Expected 'html', 'render', 'attach', 'const', or 'debug'
## expected_token
> Expected token %token%
@ -227,6 +235,10 @@ The same applies to components:
> Expected whitespace
## illegal_await_expression
> `use:`, `transition:` and `animate:` directives, attachments and bindings do not support await expressions
## illegal_element_attribute
> `<%name%>` does not support non-event attributes or spread attributes
@ -399,7 +411,7 @@ HTML restricts where certain elements can appear. In case of a violation the bro
## svelte_options_invalid_customelement
> "customElement" must be a string literal defining a valid custom element name or an object of the form { tag?: string; shadow?: "open" | "none"; props?: { [key: string]: { attribute?: string; reflect?: boolean; type: .. } } }
> "customElement" must be a string literal defining a valid custom element name or an object of the form { tag?: string; shadow?: "open" | "none" | `ShadowRootInit`; props?: { [key: string]: { attribute?: string; reflect?: boolean; type: .. } } }
## svelte_options_invalid_customelement_props
@ -407,7 +419,9 @@ HTML restricts where certain elements can appear. In case of a violation the bro
## svelte_options_invalid_customelement_shadow
> "shadow" must be either "open" or "none"
> "shadow" must be either "open", "none" or `ShadowRootInit` object.
See https://developer.mozilla.org/en-US/docs/Web/API/Element/attachShadow#options for more information on valid shadow root constructor options
## svelte_options_invalid_tagname

@ -66,7 +66,7 @@ Coding for the keyboard is important for users with physical disabilities who ca
## a11y_consider_explicit_label
> Buttons and links should either contain text or have an `aria-label` or `aria-labelledby` attribute
> Buttons and links should either contain text or have an `aria-label`, `aria-labelledby` or `title` attribute
## a11y_distracting_elements

@ -0,0 +1,60 @@
## async_local_storage_unavailable
> The node API `AsyncLocalStorage` is not available, but is required to use async server rendering.
Some platforms require configuration flags to enable this API. Consult your platform's documentation.
## await_invalid
> 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.
## html_deprecated
> The `html` property of server render results has been deprecated. Use `body` instead.
## hydratable_clobbering
> Attempted to set `hydratable` with key `%key%` twice with different values.
>
> %stack%
This error occurs when using `hydratable` multiple times with the same key. To avoid this, you can:
- Ensure all invocations with the same key result in the same value
- Update the keys to make both instances unique
```svelte
<script>
import { hydratable } from 'svelte';
// which one should "win" and be serialized in the rendered response?
const one = hydratable('not-unique', () => 1);
const two = hydratable('not-unique', () => 2);
</script>
```
## hydratable_serialization_failed
> Failed to serialize `hydratable` data for key `%key%`.
>
> `hydratable` can serialize anything [`uneval` from `devalue`](https://npmjs.com/package/uneval) can, plus Promises.
>
> Cause:
> %stack%
## invalid_csp
> `csp.nonce` was set while `csp.hash` was `true`. These options cannot be used simultaneously.
## lifecycle_function_unavailable
> `%name%(...)` is not available on the server
Certain methods such as `mount` cannot be invoked while running in a server context. Avoid calling them eagerly, i.e. not during render.
## server_context_required
> Could not resolve `render` context.
Certain functions such as `hydratable` cannot be invoked outside of a `render(...)` call, such as at the top level of a module.

@ -1,5 +0,0 @@
## lifecycle_function_unavailable
> `%name%(...)` is not available on the server
Certain methods such as `mount` cannot be invoked while running in a server context. Avoid calling them eagerly, i.e. not during render.

@ -0,0 +1,30 @@
## unresolved_hydratable
> A `hydratable` value with key `%key%` was created, but at least part of it was not used during the render.
>
> The `hydratable` was initialized in:
> %stack%
The most likely cause of this is creating a `hydratable` in the `script` block of your component and then `await`ing
the result inside a `svelte:boundary` with a `pending` snippet:
```svelte
<script>
import { hydratable } from 'svelte';
import { getUser } from '$lib/get-user.js';
const user = hydratable('user', getUser);
</script>
<svelte:boundary>
<h1>{(await user).name}</h1>
{#snippet pending()}
<div>Loading...</div>
{/snippet}
</svelte:boundary>
```
Consider inlining the `hydratable` call inside the boundary so that it's not called on the server.
Note that this can also happen when a `hydratable` contains multiple promises and some but not all of them have been used.

@ -1,20 +1,6 @@
## await_outside_boundary
## experimental_async_required
> Cannot await outside a `<svelte:boundary>` with a `pending` snippet
The `await` keyword can only appear in a `$derived(...)` or template expression, or at the top level of a component's `<script>` block, if it is inside a [`<svelte:boundary>`](/docs/svelte/svelte-boundary) that has a `pending` snippet:
```svelte
<svelte:boundary>
<p>{await getData()}</p>
{#snippet pending()}
<p>loading...</p>
{/snippet}
</svelte:boundary>
```
This restriction may be lifted in a future version of Svelte.
> Cannot use `%name%(...)` unless the `experimental.async` compiler option is `true`
## invalid_default_snippet
@ -70,6 +56,12 @@ Certain lifecycle methods can only be used during component initialisation. To f
<button onclick={handleClick}>click me</button>
```
## missing_context
> Context was not set in a parent component
The [`createContext()`](svelte#createContext) utility returns a `[get, set]` pair of functions. `get` will throw an error if `set` was not used to set the context in a parent component.
## snippet_without_render_tag
> Attempted to render a snippet without a `{@render}` block. This would cause the snippet code to be stringified instead of its content being rendered to the DOM. To fix this, change `{snippet}` to `{@render snippet()}`.

@ -2,7 +2,7 @@
"name": "svelte",
"description": "Cybernetically enhanced web apps",
"license": "MIT",
"version": "5.38.7",
"version": "5.49.0",
"type": "module",
"types": "./types/index.d.ts",
"engines": {
@ -158,7 +158,7 @@
"@types/aria-query": "^5.0.4",
"@types/node": "^20.11.5",
"dts-buddy": "^0.5.5",
"esbuild": "^0.21.5",
"esbuild": "^0.25.10",
"rollup": "^4.22.4",
"source-map": "^0.7.4",
"tinyglobby": "^0.2.12",
@ -174,8 +174,9 @@
"aria-query": "^5.3.1",
"axobject-query": "^4.1.0",
"clsx": "^2.1.1",
"devalue": "^5.6.2",
"esm-env": "^1.2.1",
"esrap": "^2.1.0",
"esrap": "^2.2.2",
"is-reference": "^3.0.3",
"locate-character": "^3.0.0",
"magic-string": "^0.30.11",

@ -401,6 +401,7 @@ function run() {
transform('client-warnings', 'src/internal/client/warnings.js');
transform('client-errors', 'src/internal/client/errors.js');
transform('server-warnings', 'src/internal/server/warnings.js');
transform('server-errors', 'src/internal/server/errors.js');
transform('shared-errors', 'src/internal/shared/errors.js');
transform('shared-warnings', 'src/internal/shared/warnings.js');

@ -0,0 +1,20 @@
import { DEV } from 'esm-env';
var bold = 'font-weight: bold';
var normal = 'font-weight: normal';
/**
* MESSAGE
* @param {string} PARAMETER
*/
export function CODE(PARAMETER) {
if (DEV) {
console.warn(
`%c[svelte] ${'CODE'}\n%c${MESSAGE}\nhttps://svelte.dev/e/${'CODE'}`,
bold,
normal
);
} else {
console.warn(`https://svelte.dev/e/${'CODE'}`);
}
}

@ -85,20 +85,34 @@ declare namespace $state {
? NonReactive<T>
: T extends { toJSON(): infer R }
? R
: T extends Array<infer U>
? Array<Snapshot<U>>
: T extends object
? T extends { [key: string]: any }
? { [K in keyof T]: Snapshot<T[K]> }
: never
: never;
: T extends readonly unknown[]
? { [K in keyof T]: Snapshot<T[K]> }
: T extends Array<infer U>
? Array<Snapshot<U>>
: T extends object
? T extends { [key: string]: any }
? { [K in keyof T]: Snapshot<T[K]> }
: never
: never;
/**
* Returns the latest `value`, even if the rest of the UI is suspending
* while async work (such as data loading) completes.
*
* ```svelte
* <nav>
* <a href="/" aria-current={$state.eager(pathname) === '/' ? 'page' : null}>home</a>
* <a href="/about" aria-current={$state.eager(pathname) === '/about' ? 'page' : null}>about</a>
* </nav>
* ```
*/
export function eager<T>(value: T): T;
/**
* Declares state that is _not_ made deeply reactive instead of mutating it,
* you must reassign it.
*
* Example:
* ```ts
* ```svelte
* <script>
* let items = $state.raw([0]);
*
@ -107,7 +121,7 @@ declare namespace $state {
* };
* </script>
*
* <button on:click={addItem}>
* <button onclick={addItem}>
* {items.join(', ')}
* </button>
* ```
@ -122,7 +136,7 @@ declare namespace $state {
* To take a static snapshot of a deeply reactive `$state` proxy, use `$state.snapshot`:
*
* Example:
* ```ts
* ```svelte
* <script>
* let counter = $state({ count: 0 });
*
@ -159,6 +173,9 @@ declare namespace $state {
export const prototype: never;
/** @deprecated */
export const toString: never;
// needed to keep private stuff private
export {};
}
/**
@ -216,6 +233,9 @@ declare namespace $derived {
export const prototype: never;
/** @deprecated */
export const toString: never;
// needed to keep private stuff private
export {};
}
/**
@ -332,6 +352,9 @@ declare namespace $effect {
export const prototype: never;
/** @deprecated */
export const toString: never;
// needed to keep private stuff private
export {};
}
/**
@ -375,6 +398,9 @@ declare namespace $props {
export const prototype: never;
/** @deprecated */
export const toString: never;
// needed to keep private stuff private
export {};
}
/**
@ -409,6 +435,9 @@ declare namespace $bindable {
export const prototype: never;
/** @deprecated */
export const toString: never;
// needed to keep private stuff private
export {};
}
/**
@ -471,6 +500,9 @@ declare namespace $inspect {
export const prototype: never;
/** @deprecated */
export const toString: never;
// needed to keep private stuff private
export {};
}
/**
@ -515,4 +547,7 @@ declare namespace $host {
export const prototype: never;
/** @deprecated */
export const toString: never;
// needed to keep private stuff private
export {};
}

@ -977,22 +977,22 @@ export function const_tag_invalid_expression(node) {
}
/**
* `{@const}` must be the immediate child of `{#snippet}`, `{#if}`, `{:else if}`, `{:else}`, `{#each}`, `{:then}`, `{:catch}`, `<svelte:fragment>`, `<svelte:boundary` or `<Component>`
* `{@const}` must be the immediate child of `{#snippet}`, `{#if}`, `{:else if}`, `{:else}`, `{#each}`, `{:then}`, `{:catch}`, `<svelte:fragment>`, `<svelte:boundary>` or `<Component>`
* @param {null | number | NodeLike} node
* @returns {never}
*/
export function const_tag_invalid_placement(node) {
e(node, 'const_tag_invalid_placement', `\`{@const}\` must be the immediate child of \`{#snippet}\`, \`{#if}\`, \`{:else if}\`, \`{:else}\`, \`{#each}\`, \`{:then}\`, \`{:catch}\`, \`<svelte:fragment>\`, \`<svelte:boundary\` or \`<Component>\`\nhttps://svelte.dev/e/const_tag_invalid_placement`);
e(node, 'const_tag_invalid_placement', `\`{@const}\` must be the immediate child of \`{#snippet}\`, \`{#if}\`, \`{:else if}\`, \`{:else}\`, \`{#each}\`, \`{:then}\`, \`{:catch}\`, \`<svelte:fragment>\`, \`<svelte:boundary>\` or \`<Component>\`\nhttps://svelte.dev/e/const_tag_invalid_placement`);
}
/**
* The `{@const %name% = ...}` declaration is not available in this snippet
* The `{@const %name% = ...}` declaration is not available in this snippet
* @param {null | number | NodeLike} node
* @param {string} name
* @returns {never}
*/
export function const_tag_invalid_reference(node, name) {
e(node, 'const_tag_invalid_reference', `The \`{@const ${name} = ...}\` declaration is not available in this snippet \nhttps://svelte.dev/e/const_tag_invalid_reference`);
e(node, 'const_tag_invalid_reference', `The \`{@const ${name} = ...}\` declaration is not available in this snippet\nhttps://svelte.dev/e/const_tag_invalid_reference`);
}
/**
@ -1023,6 +1023,15 @@ export function directive_missing_name(node, type) {
e(node, 'directive_missing_name', `\`${type}\` name cannot be empty\nhttps://svelte.dev/e/directive_missing_name`);
}
/**
* An `{#each ...}` block without an `as` clause cannot have a key
* @param {null | number | NodeLike} node
* @returns {never}
*/
export function each_key_without_as(node) {
e(node, 'each_key_without_as', `An \`{#each ...}\` block without an \`as\` clause cannot have a key\nhttps://svelte.dev/e/each_key_without_as`);
}
/**
* `</%name%>` attempted to close an element that was not open
* @param {null | number | NodeLike} node
@ -1120,6 +1129,15 @@ export function expected_pattern(node) {
e(node, 'expected_pattern', `Expected identifier or destructure pattern\nhttps://svelte.dev/e/expected_pattern`);
}
/**
* Expected 'html', 'render', 'attach', 'const', or 'debug'
* @param {null | number | NodeLike} node
* @returns {never}
*/
export function expected_tag(node) {
e(node, 'expected_tag', `Expected 'html', 'render', 'attach', 'const', or 'debug'\nhttps://svelte.dev/e/expected_tag`);
}
/**
* Expected token %token%
* @param {null | number | NodeLike} node
@ -1139,6 +1157,15 @@ export function expected_whitespace(node) {
e(node, 'expected_whitespace', `Expected whitespace\nhttps://svelte.dev/e/expected_whitespace`);
}
/**
* `use:`, `transition:` and `animate:` directives, attachments and bindings do not support await expressions
* @param {null | number | NodeLike} node
* @returns {never}
*/
export function illegal_await_expression(node) {
e(node, 'illegal_await_expression', `\`use:\`, \`transition:\` and \`animate:\` directives, attachments and bindings do not support await expressions\nhttps://svelte.dev/e/illegal_await_expression`);
}
/**
* `<%name%>` does not support non-event attributes or spread attributes
* @param {null | number | NodeLike} node
@ -1523,12 +1550,12 @@ export function svelte_options_invalid_attribute_value(node, list) {
}
/**
* "customElement" must be a string literal defining a valid custom element name or an object of the form { tag?: string; shadow?: "open" | "none"; props?: { [key: string]: { attribute?: string; reflect?: boolean; type: .. } } }
* "customElement" must be a string literal defining a valid custom element name or an object of the form { tag?: string; shadow?: "open" | "none" | `ShadowRootInit`; props?: { [key: string]: { attribute?: string; reflect?: boolean; type: .. } } }
* @param {null | number | NodeLike} node
* @returns {never}
*/
export function svelte_options_invalid_customelement(node) {
e(node, 'svelte_options_invalid_customelement', `"customElement" must be a string literal defining a valid custom element name or an object of the form { tag?: string; shadow?: "open" | "none"; props?: { [key: string]: { attribute?: string; reflect?: boolean; type: .. } } }\nhttps://svelte.dev/e/svelte_options_invalid_customelement`);
e(node, 'svelte_options_invalid_customelement', `"customElement" must be a string literal defining a valid custom element name or an object of the form { tag?: string; shadow?: "open" | "none" | \`ShadowRootInit\`; props?: { [key: string]: { attribute?: string; reflect?: boolean; type: .. } } }\nhttps://svelte.dev/e/svelte_options_invalid_customelement`);
}
/**
@ -1541,12 +1568,12 @@ export function svelte_options_invalid_customelement_props(node) {
}
/**
* "shadow" must be either "open" or "none"
* "shadow" must be either "open", "none" or `ShadowRootInit` object.
* @param {null | number | NodeLike} node
* @returns {never}
*/
export function svelte_options_invalid_customelement_shadow(node) {
e(node, 'svelte_options_invalid_customelement_shadow', `"shadow" must be either "open" or "none"\nhttps://svelte.dev/e/svelte_options_invalid_customelement_shadow`);
e(node, 'svelte_options_invalid_customelement_shadow', `"shadow" must be either "open", "none" or \`ShadowRootInit\` object.\nhttps://svelte.dev/e/svelte_options_invalid_customelement_shadow`);
}
/**

@ -3,13 +3,15 @@
/** @import { AST } from './public.js' */
import { walk as zimmerframe_walk } from 'zimmerframe';
import { convert } from './legacy.js';
import { parse as _parse } from './phases/1-parse/index.js';
import { parse as _parse, Parser } from './phases/1-parse/index.js';
import { remove_typescript_nodes } from './phases/1-parse/remove_typescript_nodes.js';
import { parse_stylesheet } from './phases/1-parse/read/style.js';
import { analyze_component, analyze_module } from './phases/2-analyze/index.js';
import { transform_component, transform_module } from './phases/3-transform/index.js';
import { validate_component_options, validate_module_options } from './validate-options.js';
import * as state from './state.js';
export { default as preprocess } from './preprocess/index.js';
export { print } from './print/index.js';
/**
* `compile` converts your `.svelte` source code into a JavaScript module that exports a component
@ -117,6 +119,29 @@ export function parse(source, { modern, loose } = {}) {
return to_public_ast(source, ast, modern);
}
/**
* 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'>}
*/
export function parseCss(source) {
source = remove_bom(source);
state.reset({ warning: () => false, filename: undefined });
state.set_source(source);
const parser = Parser.forCss(source);
const children = parse_stylesheet(parser);
return {
type: 'StyleSheet',
start: 0,
end: source.length,
children
};
}
/**
* @param {string} source
* @param {AST.Root} ast

@ -604,7 +604,7 @@ const instance_script = {
'Encountered an export declaration pattern that is not supported for automigration.'
);
// Turn export let into props. It's really really weird because export let { x: foo, z: [bar]} = ..
// means that foo and bar are the props (i.e. the leafs are the prop names), not x and z.
// means that foo and bar are the props (i.e. the leaves are the prop names), not x and z.
// const tmp = b.id(state.scope.generate('tmp'));
// const paths = extract_paths(declarator.id, tmp);
// state.props_pre.push(
@ -1058,8 +1058,6 @@ const template = {
handle_identifier(node, state, path);
},
RegularElement(node, { state, path, next }) {
migrate_slot_usage(node, path, state);
handle_events(node, state);
// Strip off any namespace from the beginning of the node name.
const node_name = node.name.replace(/[a-zA-Z-]*:/g, '');
@ -1067,8 +1065,12 @@ const template = {
let trimmed_position = node.end - 2;
while (state.str.original.charAt(trimmed_position - 1) === ' ') trimmed_position--;
state.str.remove(trimmed_position, node.end - 1);
state.str.appendRight(node.end, `</${node.name}>`);
state.str.appendLeft(node.end, `</${node.name}>`);
}
migrate_slot_usage(node, path, state);
handle_events(node, state);
next();
},
SvelteSelf(node, { state, next }) {
@ -1810,7 +1812,7 @@ function handle_events(element, state) {
}
/**
* Returns start and end of the node. If the start is preceeded with white-space-only before a line break,
* Returns start and end of the node. If the start is preceded with white-space-only before a line break,
* the start will be the start of the line.
* @param {string} source
* @param {LabeledStatement} node

@ -1,4 +1,6 @@
/** @import { AST } from '#compiler' */
/** @import { Location } from 'locate-character' */
/** @import * as ESTree from 'estree' */
// @ts-expect-error acorn type definitions are borked in the release we use
import { isIdentifierStart, isIdentifierChar } from 'acorn';
import fragment from './state/fragment.js';
@ -32,6 +34,20 @@ export class Parser {
/** */
index = 0;
/**
* Creates a minimal parser instance for CSS-only parsing.
* Skips Svelte component parsing setup.
* @param {string} source
* @returns {Parser}
*/
static forCss(source) {
const parser = Object.create(Parser.prototype);
parser.template = source;
parser.index = 0;
parser.loose = false;
return parser;
}
/** Whether we're parsing in TypeScript mode */
ts = false;
@ -115,21 +131,8 @@ export class Parser {
e.unexpected_eof(this.index);
}
if (this.root.fragment.nodes.length) {
let start = /** @type {number} */ (this.root.fragment.nodes[0].start);
while (regex_whitespace.test(template[start])) start += 1;
let end = /** @type {number} */ (
this.root.fragment.nodes[this.root.fragment.nodes.length - 1].end
);
while (regex_whitespace.test(template[end - 1])) end -= 1;
this.root.start = start;
this.root.end = end;
} else {
// @ts-ignore
this.root.start = this.root.end = null;
}
this.root.start = 0;
this.root.end = template.length;
const options_index = this.root.fragment.nodes.findIndex(
/** @param {any} thing */
@ -218,31 +221,45 @@ export class Parser {
return result;
}
/** @param {any} allow_reserved */
read_identifier(allow_reserved = false) {
/**
* @returns {ESTree.Identifier & { start: number, end: number, loc: { start: Location, end: Location } }}
*/
read_identifier() {
const start = this.index;
let end = start;
let name = '';
let i = this.index;
const code = /** @type {number} */ (this.template.codePointAt(this.index));
const code = /** @type {number} */ (this.template.codePointAt(i));
if (!isIdentifierStart(code, true)) return null;
if (isIdentifierStart(code, true)) {
let i = this.index;
end += code <= 0xffff ? 1 : 2;
i += code <= 0xffff ? 1 : 2;
while (end < this.template.length) {
const code = /** @type {number} */ (this.template.codePointAt(end));
while (i < this.template.length) {
const code = /** @type {number} */ (this.template.codePointAt(i));
if (!isIdentifierChar(code, true)) break;
i += code <= 0xffff ? 1 : 2;
}
if (!isIdentifierChar(code, true)) break;
end += code <= 0xffff ? 1 : 2;
}
const identifier = this.template.slice(this.index, (this.index = i));
name = this.template.slice(start, end);
this.index = end;
if (!allow_reserved && is_reserved(identifier)) {
e.unexpected_reserved_word(start, identifier);
if (is_reserved(name)) {
e.unexpected_reserved_word(start, name);
}
}
return identifier;
return {
type: 'Identifier',
name,
start,
end,
loc: {
start: state.locator(start),
end: state.locator(end)
}
};
}
/** @param {RegExp} pattern */

@ -1,11 +1,9 @@
/** @import { Location } from 'locate-character' */
/** @import { Pattern } from 'estree' */
/** @import { Parser } from '../index.js' */
import { match_bracket } from '../utils/bracket.js';
import { parse_expression_at } from '../acorn.js';
import { regex_not_newline_characters } from '../../patterns.js';
import * as e from '../../../errors.js';
import { locator } from '../../../state.js';
/**
* @param {Parser} parser
@ -15,20 +13,13 @@ export default function read_pattern(parser) {
const start = parser.index;
let i = parser.index;
const name = parser.read_identifier();
const id = parser.read_identifier();
if (name !== null) {
if (id.name !== '') {
const annotation = read_type_annotation(parser);
return {
type: 'Identifier',
name,
start,
loc: {
start: /** @type {Location} */ (locator(start)),
end: /** @type {Location} */ (locator(parser.index))
},
end: parser.index,
...id,
typeAnnotation: annotation
};
}

@ -133,11 +133,13 @@ export default function read_options(node) {
const shadow = properties.find(([name]) => name === 'shadow')?.[1];
if (shadow) {
const shadowdom = shadow?.value;
if (shadowdom !== 'open' && shadowdom !== 'none') {
e.svelte_options_invalid_customelement_shadow(shadow);
if (shadow.type === 'Literal' && (shadow.value === 'open' || shadow.value === 'none')) {
ce.shadow = shadow.value;
} else if (shadow.type === 'ObjectExpression') {
ce.shadow = shadow;
} else {
e.svelte_options_invalid_customelement_shadow(attribute);
}
ce.shadow = shadowdom;
}
const extend = properties.find(([name]) => name === 'extend')?.[1];

@ -6,6 +6,7 @@ import { regex_not_newline_characters } from '../../patterns.js';
import * as e from '../../../errors.js';
import * as w from '../../../warnings.js';
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*>/;
@ -39,9 +40,15 @@ export function read_script(parser, start, attributes) {
parser.acorn_error(err);
}
// TODO is this necessary?
ast.start = script_start;
if (ast.loc) {
// Acorn always uses `0` as the start of a `Program`, but for sourcemap purposes
// we need it to be the start of the `<script>` contents
({ line: ast.loc.start.line, column: ast.loc.start.column } = locator(start));
({ line: ast.loc.end.line, column: ast.loc.end.column } = locator(parser.index));
}
/** @type {'default' | 'module'} */
let context = 'default';

@ -24,10 +24,11 @@ const REGEX_HTML_COMMENT_CLOSE = /-->/;
*/
export default function read_style(parser, start, attributes) {
const content_start = parser.index;
const children = read_body(parser, '</style');
const children = read_body(parser, (p) => p.match('</style') || p.index >= p.template.length);
const content_end = parser.index;
parser.read(/^<\/style\s*>/);
parser.eat('</style', true);
parser.read(/^\s*>/);
return {
type: 'StyleSheet',
@ -46,20 +47,14 @@ export default function read_style(parser, start, attributes) {
/**
* @param {Parser} parser
* @param {string} close
* @returns {any[]}
* @param {(parser: Parser) => boolean} finished
* @returns {Array<AST.CSS.Rule | AST.CSS.Atrule>}
*/
function read_body(parser, close) {
function read_body(parser, finished) {
/** @type {Array<AST.CSS.Rule | AST.CSS.Atrule>} */
const children = [];
while (parser.index < parser.template.length) {
allow_comment_or_whitespace(parser);
if (parser.match(close)) {
return children;
}
while ((allow_comment_or_whitespace(parser), !finished(parser))) {
if (parser.match('@')) {
children.push(read_at_rule(parser));
} else {
@ -67,7 +62,7 @@ function read_body(parser, close) {
}
}
e.expected_token(parser.template.length, close);
return children;
}
/**
@ -513,8 +508,12 @@ function read_value(parser) {
if (escaped) {
value += '\\' + char;
escaped = false;
parser.index++;
continue;
} else if (char === '\\') {
escaped = true;
parser.index++;
continue;
} else if (char === quote_mark) {
quote_mark = null;
} else if (char === ')') {
@ -627,3 +626,12 @@ function allow_comment_or_whitespace(parser) {
parser.allow_whitespace();
}
}
/**
* Parse standalone CSS content (not wrapped in `<style>`).
* @param {Parser} parser
* @returns {Array<AST.CSS.Rule | AST.CSS.Atrule>}
*/
export function parse_stylesheet(parser) {
return read_body(parser, (p) => p.index >= p.template.length);
}

@ -27,6 +27,9 @@ const visitors = {
delete n.typeArguments;
delete n.returnType;
delete n.accessibility;
delete n.readonly;
delete n.definite;
delete n.override;
},
Decorator(node) {
e.typescript_invalid_feature(node, 'decorators (related TSC proposal is not stage 4 yet)');
@ -132,7 +135,14 @@ const visitors = {
if (node.declare) {
return b.empty;
}
delete node.abstract;
delete node.implements;
delete node.superTypeArguments;
return context.next();
},
ClassExpression(node, context) {
delete node.implements;
delete node.superTypeArguments;
return context.next();
},
MethodDefinition(node, context) {

@ -1,4 +1,5 @@
/** @import { Expression } from 'estree' */
/** @import { Expression, Identifier, SourceLocation } from 'estree' */
/** @import { Location } from 'locate-character' */
/** @import { AST } from '#compiler' */
/** @import { Parser } from '../index.js' */
import { is_void } from '../../../../utils.js';
@ -9,11 +10,12 @@ import { decode_character_references } from '../utils/html.js';
import * as e from '../../../errors.js';
import * as w from '../../../warnings.js';
import { create_fragment } from '../utils/create.js';
import { create_attribute, create_expression_metadata, is_element_node } from '../../nodes.js';
import { create_attribute, ExpressionMetadata, is_element_node } from '../../nodes.js';
import { get_attribute_expression, is_expression_attribute } from '../../../utils/ast.js';
import { closing_tag_omitted } from '../../../../html-tree-validation.js';
import { list } from '../../../utils/string.js';
import { regex_whitespace } from '../../patterns.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;
@ -68,10 +70,9 @@ export default function element(parser) {
return;
}
const is_closing_tag = parser.eat('/');
const name = parser.read_until(regex_whitespace_or_slash_or_closing_tag);
if (parser.eat('/')) {
const name = parser.read_until(regex_whitespace_or_slash_or_closing_tag);
if (is_closing_tag) {
parser.allow_whitespace();
parser.eat('>', true);
@ -126,39 +127,41 @@ export default function element(parser) {
return;
}
if (name.startsWith('svelte:') && !meta_tags.has(name)) {
const bounds = { start: start + 1, end: start + 1 + name.length };
const tag = read_tag(parser, regex_whitespace_or_slash_or_closing_tag);
if (tag.name.startsWith('svelte:') && !meta_tags.has(tag.name)) {
const bounds = { start: start + 1, end: start + 1 + tag.name.length };
e.svelte_meta_invalid_tag(bounds, list(Array.from(meta_tags.keys())));
}
if (!regex_valid_element_name.test(name) && !regex_valid_component_name.test(name)) {
if (!regex_valid_element_name.test(tag.name) && !regex_valid_component_name.test(tag.name)) {
// <div. -> in the middle of typing -> allow in loose mode
if (!parser.loose || !name.endsWith('.')) {
const bounds = { start: start + 1, end: start + 1 + name.length };
if (!parser.loose || !tag.name.endsWith('.')) {
const bounds = { start: start + 1, end: start + 1 + tag.name.length };
e.tag_invalid_name(bounds);
}
}
if (root_only_meta_tags.has(name)) {
if (name in parser.meta_tags) {
e.svelte_meta_duplicate(start, name);
if (root_only_meta_tags.has(tag.name)) {
if (tag.name in parser.meta_tags) {
e.svelte_meta_duplicate(start, tag.name);
}
if (parent.type !== 'Root') {
e.svelte_meta_invalid_placement(start, name);
e.svelte_meta_invalid_placement(start, tag.name);
}
parser.meta_tags[name] = true;
parser.meta_tags[tag.name] = true;
}
const type = meta_tags.has(name)
? meta_tags.get(name)
: regex_valid_component_name.test(name) || (parser.loose && name.endsWith('.'))
const type = meta_tags.has(tag.name)
? meta_tags.get(tag.name)
: regex_valid_component_name.test(tag.name) || (parser.loose && tag.name.endsWith('.'))
? 'Component'
: name === 'title' && parent_is_head(parser.stack)
: tag.name === 'title' && parent_is_head(parser.stack)
? 'TitleElement'
: // TODO Svelte 6/7: once slots are removed in favor of snippets, always keep slot as a regular element
name === 'slot' && !parent_is_shadowroot_template(parser.stack)
tag.name === 'slot' && !parent_is_shadowroot_template(parser.stack)
? 'SlotElement'
: 'RegularElement';
@ -169,7 +172,8 @@ export default function element(parser) {
type,
start,
end: -1,
name,
name: tag.name,
name_loc: tag.loc,
attributes: [],
fragment: create_fragment(true),
metadata: {
@ -177,14 +181,16 @@ export default function element(parser) {
mathml: false,
scoped: false,
has_spread: false,
path: []
path: [],
synthetic_value_node: null
}
}
: /** @type {AST.ElementLike} */ ({
type,
start,
end: -1,
name,
name: tag.name,
name_loc: tag.loc,
attributes: [],
fragment: create_fragment(true),
metadata: {
@ -194,14 +200,14 @@ export default function element(parser) {
parser.allow_whitespace();
if (parent.type === 'RegularElement' && closing_tag_omitted(parent.name, name)) {
if (parent.type === 'RegularElement' && closing_tag_omitted(parent.name, tag.name)) {
const end = parent.fragment.nodes[0]?.start ?? start;
w.element_implicitly_closed({ start: parent.start, end }, `<${name}>`, `</${parent.name}>`);
w.element_implicitly_closed({ start: parent.start, end }, `<${tag.name}>`, `</${parent.name}>`);
parent.end = start;
parser.pop();
parser.last_auto_closed_tag = {
tag: parent.name,
reason: name,
reason: tag.name,
depth: parser.stack.length
};
}
@ -211,7 +217,7 @@ export default function element(parser) {
const current = parser.current();
const is_top_level_script_or_style =
(name === 'script' || name === 'style') && current.type === 'Root';
(tag.name === 'script' || tag.name === 'style') && current.type === 'Root';
const read = is_top_level_script_or_style ? read_static_attribute : read_attribute;
@ -242,6 +248,10 @@ export default function element(parser) {
parser.allow_whitespace();
}
if (element.type === 'Component') {
element.metadata.expression = new ExpressionMetadata();
}
if (element.type === 'SvelteComponent') {
const index = element.attributes.findIndex(
/** @param {any} attr */
@ -257,6 +267,7 @@ export default function element(parser) {
}
element.expression = get_attribute_expression(definition);
element.metadata.expression = new ExpressionMetadata();
}
if (element.type === 'SvelteElement') {
@ -296,7 +307,7 @@ export default function element(parser) {
element.tag = get_attribute_expression(definition);
}
element.metadata.expression = create_expression_metadata();
element.metadata.expression = new ExpressionMetadata();
}
if (is_top_level_script_or_style) {
@ -319,7 +330,7 @@ export default function element(parser) {
}
}
if (name === 'script') {
if (tag.name === 'script') {
const content = read_script(parser, start, element.attributes);
if (prev_comment) {
// We take advantage of the fact that the root will never have leadingComments set,
@ -347,7 +358,7 @@ export default function element(parser) {
parser.append(element);
const self_closing = parser.eat('/') || is_void(name);
const self_closing = parser.eat('/') || is_void(tag.name);
const closed = parser.eat('>', true, false);
// Loose parsing mode
@ -377,7 +388,7 @@ export default function element(parser) {
if (self_closing || !closed) {
// don't push self-closing elements onto the stack
element.end = parser.index;
} else if (name === 'textarea') {
} else if (tag.name === 'textarea') {
// special case
element.fragment.nodes = read_sequence(
parser,
@ -386,10 +397,10 @@ export default function element(parser) {
);
parser.read(regex_closing_textarea_tag);
element.end = parser.index;
} else if (name === 'script' || name === 'style') {
} else if (tag.name === 'script' || tag.name === 'style') {
// special case
const start = parser.index;
const data = parser.read_until(new RegExp(`</${name}>`));
const data = parser.read_until(new RegExp(`</${tag.name}>`));
const end = parser.index;
/** @type {AST.Text} */
@ -402,7 +413,7 @@ export default function element(parser) {
};
element.fragment.nodes.push(node);
parser.eat(`</${name}>`, true);
parser.eat(`</${tag.name}>`, true);
element.end = parser.index;
} else {
parser.stack.push(element);
@ -445,8 +456,8 @@ function parent_is_shadowroot_template(stack) {
function read_static_attribute(parser) {
const start = parser.index;
const name = parser.read_until(regex_token_ending_character);
if (!name) return null;
const tag = read_tag(parser, regex_token_ending_character);
if (!tag.name) return null;
/** @type {true | Array<AST.Text | AST.ExpressionTag>} */
let value = true;
@ -480,7 +491,7 @@ function read_static_attribute(parser) {
e.expected_token(parser.index, '=');
}
return create_attribute(name, start, parser.index, value);
return create_attribute(tag.name, tag.loc, start, parser.index, value);
}
/**
@ -507,7 +518,7 @@ function read_attribute(parser) {
end: parser.index,
expression,
metadata: {
expression: create_expression_metadata()
expression: new ExpressionMetadata()
}
};
@ -527,16 +538,15 @@ function read_attribute(parser) {
end: parser.index,
expression,
metadata: {
expression: create_expression_metadata()
expression: new ExpressionMetadata()
}
};
return spread;
} else {
const value_start = parser.index;
let name = parser.read_identifier();
const id = parser.read_identifier();
if (name === null) {
if (id.name === '') {
if (
parser.loose &&
(parser.match('#') || parser.match('/') || parser.match('@') || parser.match(':'))
@ -546,7 +556,6 @@ function read_attribute(parser) {
return null;
} else if (parser.loose && parser.match('}')) {
// Likely in the middle of typing, just created the shorthand
name = '';
} else {
e.attribute_empty_shorthand(start);
}
@ -558,32 +567,28 @@ function read_attribute(parser) {
/** @type {AST.ExpressionTag} */
const expression = {
type: 'ExpressionTag',
start: value_start,
end: value_start + name.length,
expression: {
start: value_start,
end: value_start + name.length,
type: 'Identifier',
name
},
start: id.start,
end: id.end,
expression: id,
metadata: {
expression: create_expression_metadata()
expression: new ExpressionMetadata()
}
};
return create_attribute(name, start, parser.index, expression);
return create_attribute(id.name, id.loc, start, parser.index, expression);
}
}
const name = parser.read_until(regex_token_ending_character);
if (!name) return null;
const tag = read_tag(parser, regex_token_ending_character);
if (!tag.name) return null;
let end = parser.index;
parser.allow_whitespace();
const colon_index = name.indexOf(':');
const type = colon_index !== -1 && get_directive_type(name.slice(0, colon_index));
const colon_index = tag.name.indexOf(':');
const type = colon_index !== -1 && get_directive_type(tag.name.slice(0, colon_index));
/** @type {true | AST.ExpressionTag | Array<AST.Text | AST.ExpressionTag>} */
let value = true;
@ -612,10 +617,10 @@ function read_attribute(parser) {
}
if (type) {
const [directive_name, ...modifiers] = name.slice(colon_index + 1).split('|');
const [directive_name, ...modifiers] = tag.name.slice(colon_index + 1).split('|');
if (directive_name === '') {
e.directive_missing_name({ start, end: start + colon_index + 1 }, name);
e.directive_missing_name({ start, end: start + colon_index + 1 }, tag.name);
}
if (type === 'StyleDirective') {
@ -624,10 +629,11 @@ function read_attribute(parser) {
end,
type,
name: directive_name,
name_loc: tag.loc,
modifiers: /** @type {Array<'important'>} */ (modifiers),
value,
metadata: {
expression: create_expression_metadata()
expression: new ExpressionMetadata()
}
};
}
@ -649,23 +655,23 @@ function read_attribute(parser) {
}
}
/** @type {AST.Directive} */
const directive = {
const directive = /** @type {AST.Directive} */ ({
start,
end,
type,
name: directive_name,
name_loc: tag.loc,
expression,
metadata: {
expression: create_expression_metadata()
expression: new ExpressionMetadata()
}
};
});
// @ts-expect-error we do this separately from the declaration to avoid upsetting typescript
directive.modifiers = modifiers;
if (directive.type === 'TransitionDirective') {
const direction = name.slice(0, colon_index);
const direction = tag.name.slice(0, colon_index);
directive.intro = direction === 'in' || direction === 'transition';
directive.outro = direction === 'out' || direction === 'transition';
}
@ -686,7 +692,7 @@ function read_attribute(parser) {
return directive;
}
return create_attribute(name, start, end, value);
return create_attribute(tag.name, tag.loc, start, end, value);
}
/**
@ -823,7 +829,7 @@ function read_sequence(parser, done, location) {
end: parser.index,
expression,
metadata: {
expression: create_expression_metadata()
expression: new ExpressionMetadata()
}
};
@ -847,3 +853,25 @@ function read_sequence(parser, done, location) {
e.unexpected_eof(parser.template.length);
}
}
/**
* @param {Parser} parser
* @param {RegExp} regex
* @returns {Identifier & { start: number, end: number, loc: SourceLocation }}
*/
function read_tag(parser, regex) {
const start = parser.index;
const name = parser.read_until(regex);
const end = parser.index;
return {
type: 'Identifier',
name,
start,
end,
loc: {
start: locator(start),
end: locator(end)
}
};
}

@ -3,7 +3,7 @@
/** @import { Parser } from '../index.js' */
import { walk } from 'zimmerframe';
import * as e from '../../../errors.js';
import { create_expression_metadata } from '../../nodes.js';
import { ExpressionMetadata } from '../../nodes.js';
import { parse_expression_at } from '../acorn.js';
import read_pattern from '../read/context.js';
import read_expression, { get_loose_identifier } from '../read/expression.js';
@ -42,7 +42,7 @@ export default function tag(parser) {
end: parser.index,
expression,
metadata: {
expression: create_expression_metadata()
expression: new ExpressionMetadata()
}
});
}
@ -65,7 +65,7 @@ function open(parser) {
consequent: create_fragment(),
alternate: null,
metadata: {
expression: create_expression_metadata()
expression: new ExpressionMetadata()
}
});
@ -177,7 +177,7 @@ function open(parser) {
if (parser.eat(',')) {
parser.allow_whitespace();
index = parser.read_identifier();
index = parser.read_identifier().name;
if (!index) {
e.expected_identifier(parser.index);
}
@ -249,7 +249,7 @@ function open(parser) {
then: null,
catch: null,
metadata: {
expression: create_expression_metadata()
expression: new ExpressionMetadata()
}
});
@ -334,7 +334,7 @@ function open(parser) {
expression,
fragment: create_fragment(),
metadata: {
expression: create_expression_metadata()
expression: new ExpressionMetadata()
}
});
@ -347,16 +347,10 @@ function open(parser) {
if (parser.eat('snippet')) {
parser.require_whitespace();
const name_start = parser.index;
let name = parser.read_identifier();
const name_end = parser.index;
const id = parser.read_identifier();
if (name === null) {
if (parser.loose) {
name = '';
} else {
e.expected_identifier(parser.index);
}
if (id.name === '' && !parser.loose) {
e.expected_identifier(parser.index);
}
parser.allow_whitespace();
@ -415,12 +409,7 @@ function open(parser) {
type: 'SnippetBlock',
start,
end: -1,
expression: {
type: 'Identifier',
start: name_start,
end: name_end,
name
},
expression: id,
typeParams: type_params,
parameters: function_expression.params,
body: create_fragment(),
@ -477,7 +466,7 @@ function next(parser) {
consequent: create_fragment(),
alternate: null,
metadata: {
expression: create_expression_metadata()
expression: new ExpressionMetadata()
}
});
@ -643,7 +632,7 @@ function special(parser) {
end: parser.index,
expression,
metadata: {
expression: create_expression_metadata()
expression: new ExpressionMetadata()
}
});
@ -721,9 +710,10 @@ function special(parser) {
end: parser.index - 1
},
metadata: {
expression: create_expression_metadata()
expression: new ExpressionMetadata()
}
});
return;
}
if (parser.eat('render')) {
@ -748,12 +738,14 @@ function special(parser) {
end: parser.index,
expression: /** @type {AST.RenderTag['expression']} */ (expression),
metadata: {
expression: create_expression_metadata(),
expression: new ExpressionMetadata(),
dynamic: false,
arguments: [],
path: [],
snippets: new Set()
}
});
return;
}
e.expected_tag(parser.index);
}

@ -10,8 +10,7 @@ export function create_fragment(transparent = false) {
nodes: [],
metadata: {
transparent,
dynamic: false,
has_await: false
dynamic: false
}
};
}

@ -770,7 +770,39 @@ function get_ancestor_elements(node, adjacent_only, seen = new Set()) {
}
if (parent.type === 'RegularElement' || parent.type === 'SvelteElement') {
// Special handling for <option> inside <select>: elements inside <option> should
// also be considered descendants of <selectedcontent>, which clones the selected option's content
if (parent.type === 'RegularElement' && parent.name === 'option') {
const is_direct_child = ancestors.length === 0;
const select_element = path.findLast(
(element, j) => element.type === 'RegularElement' && element.name === 'select' && j < i
);
if (select_element && (!adjacent_only || is_direct_child)) {
/** @type {Compiler.AST.RegularElement | null} */
let selectedcontent_element = null;
walk(select_element, null, {
RegularElement(child, context) {
if (child.name === 'selectedcontent') {
selectedcontent_element = child;
context.stop();
return;
}
context.next();
}
});
if (adjacent_only && is_direct_child && selectedcontent_element) {
return [selectedcontent_element, parent];
} else if (selectedcontent_element) {
ancestors.push(selectedcontent_element);
}
}
}
ancestors.push(parent);
if (adjacent_only) {
break;
}
@ -817,6 +849,34 @@ function get_descendant_elements(node, adjacent_only, seen = new Set()) {
walk_children(node.type === 'RenderTag' ? node : node.fragment);
// Special handling for <selectedcontent>: it clones the content of the selected <option>,
// so descendants of <option> elements in the same <select> should also be considered descendants
if (node.type === 'RegularElement' && node.name === 'selectedcontent') {
const path = node.metadata.path;
const select_element = path.findLast(
(/** @type {Compiler.AST.SvelteNode} */ element) =>
element.type === 'RegularElement' && element.name === 'select'
);
if (select_element) {
walk(
select_element,
{ inside_option: false },
{
_(child, context) {
if (child.type === 'RegularElement' && child.name === 'option') {
context.next({ inside_option: true });
} else if (context.state.inside_option) {
walk_children(child);
} else {
context.next();
}
}
}
);
}
}
return descendants;
}

@ -1,4 +1,4 @@
/** @import { Expression, Node, Program } from 'estree' */
/** @import * as ESTree from 'estree' */
/** @import { Binding, AST, ValidatedCompileOptions, ValidatedModuleCompileOptions } from '#compiler' */
/** @import { AnalysisState, Visitors } from './types' */
/** @import { Analysis, ComponentAnalysis, Js, ReactiveStatement, Template } from '../types' */
@ -6,7 +6,12 @@ import { walk } from 'zimmerframe';
import { parse } from '../1-parse/acorn.js';
import * as e from '../../errors.js';
import * as w from '../../warnings.js';
import { extract_identifiers } from '../../utils/ast.js';
import {
extract_identifiers,
has_await_expression,
object,
unwrap_pattern
} from '../../utils/ast.js';
import * as b from '#compiler/builders';
import { Scope, ScopeRoot, create_scopes, get_rune, set_scope } from '../scope.js';
import check_graph_for_cycles from './utils/check_graph_for_cycles.js';
@ -19,6 +24,7 @@ import { extract_svelte_ignore } from '../../utils/extract_svelte_ignore.js';
import { ignore_map, ignore_stack, 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';
import { AttachTag } from './visitors/AttachTag.js';
import { Attribute } from './visitors/Attribute.js';
import { AwaitBlock } from './visitors/AwaitBlock.js';
@ -137,6 +143,7 @@ const visitors = {
pop_ignore();
}
},
AnimateDirective,
ArrowFunctionExpression,
AssignmentExpression,
AttachTag,
@ -206,7 +213,7 @@ const visitors = {
* @returns {Js}
*/
function js(script, root, allow_reactive_declarations, parent) {
/** @type {Program} */
/** @type {ESTree.Program} */
const ast = script?.content ?? {
type: 'Program',
sourceType: 'module',
@ -278,7 +285,8 @@ export function analyze_module(source, options) {
tracing: false,
async_deriveds: new Set(),
comments,
classes: new Map()
classes: new Map(),
pickled_awaits: new Set()
};
state.adjust({
@ -288,7 +296,7 @@ export function analyze_module(source, options) {
});
walk(
/** @type {Node} */ (ast),
/** @type {ESTree.Node} */ (ast),
{
scope,
scopes,
@ -304,7 +312,8 @@ export function analyze_module(source, options) {
options: /** @type {ValidatedCompileOptions} */ (options),
fragment: null,
parent_element: null,
reactive_statement: null
reactive_statement: null,
derived_function_depth: -1
},
visitors
);
@ -345,7 +354,7 @@ export function analyze_component(root, source, options) {
const store_name = name.slice(1);
const declaration = instance.scope.get(store_name);
const init = /** @type {Node | undefined} */ (declaration?.initial);
const init = /** @type {ESTree.Node | undefined} */ (declaration?.initial);
// If we're not in legacy mode through the compiler option, assume the user
// is referencing a rune and not a global store.
@ -405,7 +414,7 @@ export function analyze_component(root, source, options) {
/** @type {number} */ (node.start) > /** @type {number} */ (module.ast.start) &&
/** @type {number} */ (node.end) < /** @type {number} */ (module.ast.end) &&
// const state = $state(0) is valid
get_rune(/** @type {Node} */ (path.at(-1)), module.scope) === null
get_rune(/** @type {ESTree.Node} */ (path.at(-1)), module.scope) === null
) {
e.store_invalid_subscription(node);
}
@ -456,10 +465,19 @@ export function analyze_component(root, source, options) {
const is_custom_element = !!options.customElementOptions || options.customElement;
const name = module.scope.generate(options.name ?? component_name);
state.adjust({
component_name: name,
dev: options.dev,
rootDir: options.rootDir,
runes
});
// TODO remove all the ?? stuff, we don't need it now that we're validating the config
/** @type {ComponentAnalysis} */
const analysis = {
name: module.scope.generate(options.name ?? component_name),
name,
root: scope_root,
module,
instance,
@ -520,7 +538,7 @@ export function analyze_component(root, source, options) {
hash: root.css
? options.cssHash({
css: root.css.content.styles,
filename: options.filename,
filename: state.filename,
name: component_name,
hash
})
@ -531,16 +549,16 @@ export function analyze_component(root, source, options) {
source,
snippet_renderers: new Map(),
snippets: new Set(),
async_deriveds: new Set()
async_deriveds: new Set(),
pickled_awaits: new Set(),
instance_body: {
sync: [],
async: [],
declarations: [],
hoisted: []
}
};
state.adjust({
component_name: analysis.name,
dev: options.dev,
rootDir: options.rootDir,
runes
});
if (!runes) {
// every exported `let` or `var` declaration becomes a prop, everything else becomes an export
for (const node of instance.ast.body) {
@ -631,7 +649,7 @@ export function analyze_component(root, source, options) {
// @ts-expect-error
_: set_scope,
Identifier(node, context) {
const parent = /** @type {Expression} */ (context.path.at(-1));
const parent = /** @type {ESTree.Expression} */ (context.path.at(-1));
if (is_reference(node, parent)) {
const binding = context.state.scope.get(node.name);
@ -639,7 +657,8 @@ export function analyze_component(root, source, options) {
if (
binding &&
binding.kind === 'normal' &&
binding.declaration_kind !== 'import'
binding.declaration_kind !== 'import' &&
binding.declaration_kind !== 'function'
) {
binding.kind = 'state';
binding.mutated = true;
@ -671,6 +690,8 @@ export function analyze_component(root, source, options) {
}
}
calculate_blockers(instance, scopes, analysis);
if (analysis.runes) {
const props_refs = module.scope.references.get('$$props');
if (props_refs) {
@ -697,7 +718,8 @@ export function analyze_component(root, source, options) {
expression: null,
state_fields: new Map(),
function_depth: scope.function_depth,
reactive_statement: null
reactive_statement: null,
derived_function_depth: -1
};
walk(/** @type {AST.SvelteNode} */ (ast), state, visitors);
@ -764,7 +786,8 @@ export function analyze_component(root, source, options) {
component_slots: new Set(),
expression: null,
state_fields: new Map(),
function_depth: scope.function_depth
function_depth: scope.function_depth,
derived_function_depth: -1
};
walk(/** @type {AST.SvelteNode} */ (ast), state, visitors);
@ -878,7 +901,7 @@ export function analyze_component(root, source, options) {
// We need an empty class to generate the set_class() or class="" correctly
if (!has_spread && !has_class && (node.metadata.scoped || has_class_directive)) {
node.attributes.push(
create_attribute('class', -1, -1, [
create_attribute('class', null, -1, -1, [
{
type: 'Text',
data: '',
@ -893,7 +916,7 @@ export function analyze_component(root, source, options) {
// We need an empty style to generate the set_style() correctly
if (!has_spread && !has_style && has_style_directive) {
node.attributes.push(
create_attribute('style', -1, -1, [
create_attribute('style', null, -1, -1, [
{
type: 'Text',
data: '',
@ -912,6 +935,287 @@ export function analyze_component(root, source, options) {
return analysis;
}
/**
* Analyzes the instance's top level statements to calculate which bindings need to wait on which
* 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) {
/**
* @param {ESTree.Node} expression
* @param {Scope} scope
* @param {Set<Binding>} touched
* @param {Set<ESTree.Node>} seen
*/
const touch = (expression, scope, touched, seen = new Set()) => {
if (seen.has(expression)) return;
seen.add(expression);
walk(
expression,
{ scope },
{
ImportDeclaration(node) {},
Identifier(node, context) {
const parent = /** @type {ESTree.Node} */ (context.path.at(-1));
if (is_reference(node, parent)) {
const binding = context.state.scope.get(node.name);
if (binding) {
touched.add(binding);
for (const assignment of binding.assignments) {
touch(assignment.value, assignment.scope, touched, seen);
}
}
}
}
}
);
};
/**
* @param {ESTree.Node} node
* @param {Set<ESTree.Node>} seen
* @param {Set<Binding>} reads
* @param {Set<Binding>} writes
*/
const trace_references = (node, reads, writes, seen = new Set()) => {
if (seen.has(node)) return;
seen.add(node);
/**
* @param {ESTree.Pattern} node
* @param {Scope} scope
*/
function update(node, scope) {
for (const pattern of unwrap_pattern(node)) {
const node = object(pattern);
if (!node) return;
const binding = scope.get(node.name);
if (!binding) return;
writes.add(binding);
}
}
walk(
node,
{ scope: instance.scope },
{
_(node, context) {
const scope = scopes.get(node);
if (scope) {
context.next({ scope });
} else {
context.next();
}
},
AssignmentExpression(node, context) {
update(node.left, context.state.scope);
},
UpdateExpression(node, context) {
update(
/** @type {ESTree.Identifier | ESTree.MemberExpression} */ (node.argument),
context.state.scope
);
},
CallExpression(node, context) {
// for now, assume everything touched by the callee ends up mutating the object
// TODO optimise this better
// special case — no need to peek inside effects as they only run once async work has completed
const rune = get_rune(node, context.state.scope);
if (rune === '$effect') return;
/** @type {Set<Binding>} */
const touched = new Set();
touch(node, context.state.scope, touched);
for (const b of touched) {
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)) {
const binding = context.state.scope.get(node.name);
if (binding) {
reads.add(binding);
}
}
}
}
);
};
let awaited = false;
// TODO this should probably be attached to the scope?
const promises = b.id('$$promises');
/**
* @param {ESTree.Identifier} id
* @param {NonNullable<Binding['blocker']>} blocker
*/
function push_declaration(id, blocker) {
analysis.instance_body.declarations.push(id);
const binding = /** @type {Binding} */ (instance.scope.get(id.name));
binding.blocker = blocker;
}
/**
* Analysis of blockers for functions is deferred until we know which statements are async/blockers
* @type {Array<ESTree.FunctionDeclaration | ESTree.VariableDeclarator>}
*/
const functions = [];
for (let node of instance.ast.body) {
if (node.type === 'ImportDeclaration') {
analysis.instance_body.hoisted.push(node);
continue;
}
if (node.type === 'ExportDefaultDeclaration' || node.type === 'ExportAllDeclaration') {
// these can't exist inside `<script>` but TypeScript doesn't know that
continue;
}
if (node.type === 'ExportNamedDeclaration') {
if (node.declaration) {
node = node.declaration;
} else {
continue;
}
}
const has_await = has_await_expression(node);
awaited ||= has_await;
if (node.type === 'FunctionDeclaration') {
analysis.instance_body.sync.push(node);
functions.push(node);
} else if (node.type === 'VariableDeclaration') {
for (const declarator of node.declarations) {
if (get_rune(declarator.init, instance.scope) === '$props.id') {
// special case
continue;
}
if (
declarator.init?.type === 'ArrowFunctionExpression' ||
declarator.init?.type === 'FunctionExpression'
) {
// One declarator per declaration, makes things simpler. The ternary ensures more accurate source maps in the common case
analysis.instance_body.sync.push(
node.declarations.length === 1 ? node : b.declaration(node.kind, [declarator])
);
functions.push(declarator);
} else if (!awaited) {
// One declarator per declaration, makes things simpler. The ternary ensures more accurate source maps in the common case
analysis.instance_body.sync.push(
node.declarations.length === 1 ? node : b.declaration(node.kind, [declarator])
);
} else {
/** @type {Set<Binding>} */
const reads = new Set(); // TODO we're not actually using this yet
/** @type {Set<Binding>} */
const writes = new Set();
trace_references(declarator, reads, writes);
const blocker = /** @type {NonNullable<Binding['blocker']>} */ (
b.member(promises, b.literal(analysis.instance_body.async.length), true)
);
for (const binding of writes) {
binding.blocker = blocker;
}
for (const id of extract_identifiers(declarator.id)) {
push_declaration(id, blocker);
}
// one declarator per declaration, makes things simpler
analysis.instance_body.async.push({
node: declarator,
has_await
});
}
}
} else if (awaited) {
/** @type {Set<Binding>} */
const reads = new Set(); // TODO we're not actually using this yet
/** @type {Set<Binding>} */
const writes = new Set();
trace_references(node, reads, writes);
const blocker = /** @type {NonNullable<Binding['blocker']>} */ (
b.member(promises, b.literal(analysis.instance_body.async.length), true)
);
for (const binding of writes) {
binding.blocker = blocker;
}
if (node.type === 'ClassDeclaration') {
push_declaration(node.id, blocker);
analysis.instance_body.async.push({ node, has_await });
} else {
analysis.instance_body.async.push({ node, has_await });
}
} else {
analysis.instance_body.sync.push(node);
}
}
for (const fn of functions) {
/** @type {Set<Binding>} */
const reads_writes = new Set();
const body =
fn.type === 'VariableDeclarator'
? /** @type {ESTree.FunctionExpression | ESTree.ArrowFunctionExpression} */ (fn.init).body
: fn.body;
trace_references(body, reads_writes, reads_writes);
const max = [...reads_writes].reduce((max, binding) => {
if (binding.blocker) {
let property = /** @type {ESTree.SimpleLiteral & { value: number }} */ (
binding.blocker.property
);
return Math.max(property.value, max);
}
return max;
}, -1);
if (max === -1) continue;
const blocker = b.member(promises, b.literal(max), true);
const binding = /** @type {Binding} */ (
fn.type === 'FunctionDeclaration'
? instance.scope.get(fn.id.name)
: instance.scope.get(/** @type {ESTree.Identifier} */ (fn.id).name)
);
binding.blocker = /** @type {typeof binding['blocker']} */ (blocker);
}
}
/**
* @param {Map<import('estree').LabeledStatement, ReactiveStatement>} unsorted_reactive_declarations
*/

@ -1,6 +1,7 @@
import type { Scope } from '../scope.js';
import type { ComponentAnalysis, ReactiveStatement } from '../types.js';
import type { AST, ExpressionMetadata, StateField, ValidatedCompileOptions } from '#compiler';
import type { AST, StateField, ValidatedCompileOptions } from '#compiler';
import type { ExpressionMetadata } from '../nodes.js';
export interface AnalysisState {
scope: Scope;
@ -27,6 +28,11 @@ export interface AnalysisState {
// legacy stuff
reactive_statement: null | ReactiveStatement;
/**
* Set when we're inside a `$derived(...)` expression (but not `$derived.by(...)`) or `@const`
*/
derived_function_depth: number;
}
export type Context<State extends AnalysisState = AnalysisState> = import('zimmerframe').Context<

@ -0,0 +1,15 @@
/** @import { Context } from '../types' */
/** @import { AST } from '#compiler'; */
import * as e from '../../../errors.js';
/**
* @param {AST.AnimateDirective} node
* @param {Context} context
*/
export function AnimateDirective(node, context) {
context.next({ ...context.state, expression: node.metadata.expression });
if (node.metadata.expression.has_await) {
e.illegal_await_expression(node);
}
}

@ -1,7 +1,7 @@
/** @import { AST } from '#compiler' */
/** @import { Context } from '../types' */
import { mark_subtree_dynamic } from './shared/fragment.js';
import * as e from '../../../errors.js';
/**
* @param {AST.AttachTag} node
@ -10,4 +10,8 @@ import { mark_subtree_dynamic } from './shared/fragment.js';
export function AttachTag(node, context) {
mark_subtree_dynamic(context.path);
context.next({ ...context.state, expression: node.metadata.expression });
if (node.metadata.expression.has_await) {
e.illegal_await_expression(node);
}
}

@ -1,12 +1,7 @@
/** @import { ArrowFunctionExpression, Expression, FunctionDeclaration, FunctionExpression } from 'estree' */
/** @import { AST, DelegatedEvent } from '#compiler' */
/** @import { AST } from '#compiler' */
/** @import { Context } from '../types' */
import { cannot_be_set_statically, is_capture_event, is_delegated } from '../../../../utils.js';
import {
get_attribute_chunks,
get_attribute_expression,
is_event_attribute
} from '../../../utils/ast.js';
import { cannot_be_set_statically, can_delegate_event } from '../../../../utils.js';
import { get_attribute_chunks, is_event_attribute } from '../../../utils/ast.js';
import { mark_subtree_dynamic } from './shared/fragment.js';
/**
@ -64,181 +59,8 @@ export function Attribute(node, context) {
context.state.analysis.uses_event_attributes = true;
}
const expression = get_attribute_expression(node);
const delegated_event = get_delegated_event(node.name.slice(2), expression, context);
if (delegated_event !== null) {
if (delegated_event.hoisted) {
delegated_event.function.metadata.hoisted = true;
}
node.metadata.delegated = delegated_event;
}
}
}
}
/** @type {DelegatedEvent} */
const unhoisted = { hoisted: false };
/**
* Checks if given event attribute can be delegated/hoisted and returns the corresponding info if so
* @param {string} event_name
* @param {Expression | null} handler
* @param {Context} context
* @returns {null | DelegatedEvent}
*/
function get_delegated_event(event_name, handler, context) {
// Handle delegated event handlers. Bail out if not a delegated event.
if (!handler || !is_delegated(event_name)) {
return null;
}
// If we are not working with a RegularElement, then bail out.
const element = context.path.at(-1);
if (element?.type !== 'RegularElement') {
return null;
}
/** @type {FunctionExpression | FunctionDeclaration | ArrowFunctionExpression | null} */
let target_function = null;
let binding = null;
if (element.metadata.has_spread) {
// event attribute becomes part of the dynamic spread array
return unhoisted;
}
if (handler.type === 'ArrowFunctionExpression' || handler.type === 'FunctionExpression') {
target_function = handler;
} else if (handler.type === 'Identifier') {
binding = context.state.scope.get(handler.name);
if (context.state.analysis.module.scope.references.has(handler.name)) {
// If a binding with the same name is referenced in the module scope (even if not declared there), bail out
return unhoisted;
}
if (binding != null) {
for (const { path } of binding.references) {
const parent = path.at(-1);
if (parent === undefined) return unhoisted;
const grandparent = path.at(-2);
/** @type {AST.RegularElement | null} */
let element = null;
/** @type {string | null} */
let event_name = null;
if (parent.type === 'OnDirective') {
element = /** @type {AST.RegularElement} */ (grandparent);
event_name = parent.name;
} else if (
parent.type === 'ExpressionTag' &&
grandparent?.type === 'Attribute' &&
is_event_attribute(grandparent)
) {
element = /** @type {AST.RegularElement} */ (path.at(-3));
const attribute = /** @type {AST.Attribute} */ (grandparent);
event_name = get_attribute_event_name(attribute.name);
}
if (element && event_name) {
if (
element.type !== 'RegularElement' ||
element.metadata.has_spread ||
!is_delegated(event_name)
) {
return unhoisted;
}
} else if (parent.type !== 'FunctionDeclaration' && parent.type !== 'VariableDeclarator') {
return unhoisted;
}
}
node.metadata.delegated =
parent?.type === 'RegularElement' && can_delegate_event(node.name.slice(2));
}
// If the binding is exported, bail out
if (context.state.analysis.exports.find((node) => node.name === handler.name)) {
return unhoisted;
}
if (binding?.is_function()) {
target_function = binding.initial;
}
}
// If we can't find a function, or the function has multiple parameters, bail out
if (target_function == null || target_function.params.length > 1) {
return unhoisted;
}
const visited_references = new Set();
const scope = target_function.metadata.scope;
for (const [reference] of scope.references) {
// Bail out if the arguments keyword is used or $host is referenced
if (reference === 'arguments' || reference === '$host') return unhoisted;
// Bail out if references a store subscription
if (scope.get(`$${reference}`)?.kind === 'store_sub') return unhoisted;
const binding = scope.get(reference);
const local_binding = context.state.scope.get(reference);
// if the function access a snippet that can't be hoisted we bail out
if (
local_binding !== null &&
local_binding.initial?.type === 'SnippetBlock' &&
!local_binding.initial.metadata.can_hoist
) {
return unhoisted;
}
// If we are referencing a binding that is shadowed in another scope then bail out (unless it's declared within the function).
if (
local_binding !== null &&
binding !== null &&
local_binding.node !== binding.node &&
scope.declarations.get(reference) !== binding
) {
return unhoisted;
}
// If we have multiple references to the same store using $ prefix, bail out.
if (
binding !== null &&
binding.kind === 'store_sub' &&
visited_references.has(reference.slice(1))
) {
return unhoisted;
}
// If we reference the index within an each block, then bail out.
if (binding !== null && binding.initial?.type === 'EachBlock') return unhoisted;
if (
binding !== null &&
// Bail out if the binding is a rest param
(binding.declaration_kind === 'rest_param' ||
// Bail out if we reference anything from the EachBlock (for now) that mutates in non-runes mode,
(((!context.state.analysis.runes && binding.kind === 'each') ||
// or any normal not reactive bindings that are mutated.
binding.kind === 'normal') &&
binding.updated))
) {
return unhoisted;
}
visited_references.add(reference);
}
return { hoisted: true, function: target_function };
}
/**
* @param {string} event_name
*/
function get_attribute_event_name(event_name) {
event_name = event_name.slice(2);
if (is_capture_event(event_name)) {
event_name = event_name.slice(0, -7);
}
return event_name;
}

@ -1,5 +1,6 @@
/** @import { AwaitExpression } from 'estree' */
/** @import { AwaitExpression, Expression, SpreadElement, Property } from 'estree' */
/** @import { Context } from '../types' */
/** @import { AST } from '#compiler' */
import * as e from '../../../errors.js';
/**
@ -7,19 +8,24 @@ import * as e from '../../../errors.js';
* @param {Context} context
*/
export function AwaitExpression(node, context) {
let suspend = context.state.ast_type === 'instance' && context.state.function_depth === 1;
const tla = context.state.ast_type === 'instance' && context.state.function_depth === 1;
// preserve context for awaits that precede other expressions in template or `$derived(...)`
if (
is_reactive_expression(
context.path,
context.state.derived_function_depth === context.state.function_depth
) &&
!is_last_evaluated_expression(context.path, node)
) {
context.state.analysis.pickled_awaits.add(node);
}
let suspend = tla;
if (context.state.expression) {
context.state.expression.has_await = true;
if (
context.state.fragment &&
// TODO there's probably a better way to do this
context.path.some((node) => node.type === 'ConstTag')
) {
context.state.fragment.metadata.has_await = true;
}
suspend = true;
}
@ -37,3 +43,108 @@ export function AwaitExpression(node, context) {
context.next();
}
/**
* @param {AST.SvelteNode[]} path
* @param {boolean} in_derived
*/
export function is_reactive_expression(path, in_derived) {
if (in_derived) return true;
let i = path.length;
while (i--) {
const parent = path[i];
if (
parent.type === 'ArrowFunctionExpression' ||
parent.type === 'FunctionExpression' ||
parent.type === 'FunctionDeclaration'
) {
// No reactive expression found between function and await
return false;
}
// @ts-expect-error we could probably use a neater/more robust mechanism
if (parent.metadata) {
return true;
}
}
return false;
}
/**
* @param {AST.SvelteNode[]} path
* @param {Expression | SpreadElement | Property} node
*/
function is_last_evaluated_expression(path, node) {
let i = path.length;
while (i--) {
const parent = path[i];
if (parent.type === 'ConstTag') {
// {@const ...} tags are treated as deriveds and its contents should all get the preserve-reactivity treatment
return false;
}
// @ts-expect-error we could probably use a neater/more robust mechanism
if (parent.metadata) {
return true;
}
switch (parent.type) {
case 'ArrayExpression':
if (node !== parent.elements.at(-1)) return false;
break;
case 'AssignmentExpression':
case 'BinaryExpression':
case 'LogicalExpression':
if (node === parent.left) return false;
break;
case 'CallExpression':
case 'NewExpression':
if (node !== parent.arguments.at(-1)) return false;
break;
case 'ConditionalExpression':
if (node === parent.test) return false;
break;
case 'MemberExpression':
if (parent.computed && node === parent.object) return false;
break;
case 'ObjectExpression':
if (node !== parent.properties.at(-1)) return false;
break;
case 'Property':
if (node === parent.key) return false;
break;
case 'SequenceExpression':
if (node !== parent.expressions.at(-1)) return false;
break;
case 'TaggedTemplateExpression':
if (node !== parent.quasi.expressions.at(-1)) return false;
break;
case 'TemplateLiteral':
if (node !== parent.expressions.at(-1)) return false;
break;
case 'VariableDeclarator':
return true;
default:
return false;
}
node = parent;
}
}

@ -33,7 +33,7 @@ export function BindDirective(node, context) {
e.bind_invalid_target(
node,
node.name,
property.valid_elements.map((valid_element) => `<${valid_element}>`).join(', ')
property.valid_elements.map((valid_element) => `\`<${valid_element}>\``).join(', ')
);
}
@ -67,11 +67,15 @@ export function BindDirective(node, context) {
}
} else {
if (node.name === 'checked' && type?.value[0].data !== 'checkbox') {
e.bind_invalid_target(node, node.name, '<input type="checkbox">');
e.bind_invalid_target(
node,
node.name,
`\`<input type="checkbox">\`${type?.value[0].data === 'radio' ? ` — for \`<input type="radio">\`, use \`bind:group\`` : ''}`
);
}
if (node.name === 'files' && type?.value[0].data !== 'file') {
e.bind_invalid_target(node, node.name, '<input type="file">');
e.bind_invalid_target(node, node.name, '`<input type="file">`');
}
}
}
@ -94,7 +98,7 @@ export function BindDirective(node, context) {
e.bind_invalid_target(
node,
node.name,
`non-<svg> elements. Use 'clientWidth' for <svg> instead`
`non-\`<svg>\` elements. Use \`bind:clientWidth\` for \`<svg>\` instead`
);
}
@ -155,6 +159,22 @@ export function BindDirective(node, context) {
mark_subtree_dynamic(context.path);
const [get, set] = node.expression.expressions;
// We gotta jump across the getter/setter functions to avoid the expression metadata field being reset to null
// as we want to collect the functions' blocker/async info
context.visit(get.type === 'ArrowFunctionExpression' ? get.body : get, {
...context.state,
expression: node.metadata.expression
});
context.visit(set.type === 'ArrowFunctionExpression' ? set.body : set, {
...context.state,
expression: node.metadata.expression
});
if (node.metadata.expression.has_await) {
e.illegal_await_expression(node);
}
return;
}
@ -168,6 +188,7 @@ export function BindDirective(node, context) {
}
const binding = context.state.scope.get(left.name);
node.metadata.binding = binding;
if (assignee.type === 'Identifier') {
// reassignment
@ -242,7 +263,8 @@ export function BindDirective(node, context) {
node.metadata = {
binding_group_name: group_name,
parent_each_blocks: each_blocks
parent_each_blocks: each_blocks,
expression: node.metadata.expression
};
}
@ -250,5 +272,9 @@ export function BindDirective(node, context) {
w.bind_invalid_each_rest(binding.node, binding.node.name);
}
context.next();
context.next({ ...context.state, expression: node.metadata.expression });
if (node.metadata.expression.has_await) {
e.illegal_await_expression(node);
}
}

@ -7,7 +7,7 @@ import { get_parent } from '../../../utils/ast.js';
import { is_pure, is_safe_identifier } from './shared/utils.js';
import { dev, locate_node, source } from '../../../state.js';
import * as b from '#compiler/builders';
import { create_expression_metadata } from '../../nodes.js';
import { ExpressionMetadata } from '../../nodes.js';
/**
* @param {CallExpression} node
@ -226,6 +226,13 @@ export function CallExpression(node, context) {
break;
}
case '$state.eager':
if (node.arguments.length !== 1) {
e.rune_invalid_arguments_length(node, rune, 'exactly one argument');
}
break;
case '$state.snapshot':
if (node.arguments.length !== 1) {
e.rune_invalid_arguments_length(node, rune, 'exactly one argument');
@ -236,11 +243,12 @@ export function CallExpression(node, context) {
// `$inspect(foo)` or `$derived(foo) should not trigger the `static-state-reference` warning
if (rune === '$derived') {
const expression = create_expression_metadata();
const expression = new ExpressionMetadata();
context.next({
...context.state,
function_depth: context.state.function_depth + 1,
derived_function_depth: context.state.function_depth + 1,
expression
});

@ -16,5 +16,11 @@ export function Component(node, context) {
binding !== null &&
(binding.kind !== 'normal' || node.name.includes('.'));
if (binding) {
node.metadata.expression.has_state = node.metadata.dynamic;
node.metadata.expression.dependencies.add(binding);
node.metadata.expression.references.add(binding);
}
visit_component(node, context);
}

@ -35,5 +35,11 @@ export function ConstTag(node, context) {
const declaration = node.declaration.declarations[0];
context.visit(declaration.id);
context.visit(declaration.init, { ...context.state, expression: node.metadata.expression });
context.visit(declaration.init, {
...context.state,
expression: node.metadata.expression,
// We're treating this like a $derived under the hood
function_depth: context.state.function_depth + 1,
derived_function_depth: context.state.function_depth + 1
});
}

@ -1,3 +1,4 @@
/** @import { Expression } from 'estree' */
/** @import { AST, Binding } from '#compiler' */
/** @import { Context } from '../types' */
/** @import { Scope } from '../../scope' */
@ -28,6 +29,10 @@ export function EachBlock(node, context) {
node.key.type !== 'Identifier' || !node.index || node.key.name !== node.index;
}
if (node.metadata.keyed && !node.context) {
e.each_key_without_as(/** @type {Expression} */ (node.key));
}
// evaluate expression in parent scope
context.visit(node.expression, {
...context.state,
@ -49,7 +54,9 @@ export function EachBlock(node, context) {
// collect transitive dependencies...
for (const binding of node.metadata.expression.dependencies) {
collect_transitive_dependencies(binding, node.metadata.transitive_deps);
if (binding.declaration_kind !== 'function') {
collect_transitive_dependencies(binding, node.metadata.transitive_deps);
}
}
// ...and ensure they are marked as state, so they can be turned

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

Loading…
Cancel
Save