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

pull/17348/head
Simon H 5 months ago committed by GitHub
commit 3ab156b2bb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

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

@ -1,5 +0,0 @@
---
'svelte': patch
---
fix: correctly print `!doctype` during `print`

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

@ -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,11 +82,11 @@ 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
node-version: 24
cache: pnpm
- name: install
run: pnpm install --frozen-lockfile
@ -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

@ -1,115 +0,0 @@
name: Update pkg.pr.new comment
on:
workflow_run:
workflows: ['Publish Any Commit']
types:
- completed
permissions:
pull-requests: write
jobs:
build:
name: 'Update comment'
runs-on: ubuntu-latest
steps:
- name: Download artifact
uses: actions/download-artifact@v4
with:
name: output
github-token: ${{ secrets.GITHUB_TOKEN }}
run-id: ${{ github.event.workflow_run.id }}
- run: ls -R .
- name: 'Post or update comment'
uses: actions/github-script@v6
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const fs = require('fs');
const output = JSON.parse(fs.readFileSync('output.json', 'utf8'));
const bot_comment_identifier = `<!-- pkg.pr.new comment -->`;
const body = (number) => `${bot_comment_identifier}
[Playground](https://svelte.dev/playground?version=pr-${number})
\`\`\`
${output.packages.map((p) => `pnpm add https://pkg.pr.new/${p.name}@${number}`).join('\n')}
\`\`\`
`;
async function find_bot_comment(issue_number) {
if (!issue_number) return null;
const comments = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue_number,
});
return comments.data.find((comment) =>
comment.body.includes(bot_comment_identifier)
);
}
async function create_or_update_comment(issue_number) {
if (!issue_number) {
console.log('No issue number provided. Cannot post or update comment.');
return;
}
const existing_comment = await find_bot_comment(issue_number);
if (existing_comment) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: existing_comment.id,
body: body(issue_number),
});
} else {
await github.rest.issues.createComment({
issue_number: issue_number,
owner: context.repo.owner,
repo: context.repo.repo,
body: body(issue_number),
});
}
}
async function log_publish_info() {
const svelte_package = output.packages.find(p => p.name === 'svelte');
const svelte_sha = svelte_package.url.replace(/^.+@([^@]+)$/, '$1');
console.log('\n' + '='.repeat(50));
console.log('Publish Information');
console.log('='.repeat(50));
console.log('\nPublished Packages:');
console.log(output.packages.map((p) => `${p.name} - pnpm add https://pkg.pr.new/${p.name}@${p.url.replace(/^.+@([^@]+)$/, '$1')}`).join('\n'));
if(svelte_sha){
console.log('\nPlayground URL:');
console.log(`\nhttps://svelte.dev/playground?version=commit-${svelte_sha}`)
}
console.log('\n' + '='.repeat(50));
}
if (output.event_name === 'pull_request') {
if (output.number) {
await create_or_update_comment(output.number);
}
} else if (output.event_name === 'push') {
const pull_requests = await github.rest.pulls.list({
owner: context.repo.owner,
repo: context.repo.repo,
state: 'open',
head: `${context.repo.owner}:${output.ref.replace('refs/heads/', '')}`,
});
if (pull_requests.data.length > 0) {
await create_or_update_comment(pull_requests.data[0].number);
} else {
console.log(
'No open pull request found for this push. Logging publish information to console:'
);
await log_publish_info();
}
}

@ -1,18 +1,53 @@
name: Publish Any Commit
on: [push, pull_request]
name: pkg.pr.new
on:
pull_request_target:
types: [opened, synchronize]
push:
branches: [main]
permissions: {}
jobs:
build:
permissions: {}
# This job determines the environment to use for the build job. It ensures that:
# - For pushes to main, we use the "Publish pkg.pr.new (maintainers)" environment.
# - For PRs from the same repository, we also use the "Publish pkg.pr.new (maintainers)" environment, since these are trusted.
# - For PRs from forks, we use the "Publish pkg.pr.new (external contributors)" environment, which requires manual approval by a maintainer before the build job can run.
# This protects us from running untrusted code while still allowing external contributors to use pkg.pr.new.
resolve-env:
runs-on: ubuntu-latest
outputs:
environment: ${{ steps.resolve.outputs.environment }}
steps:
- name: Determine environment
id: resolve
run: |
if [[ "${{ github.event_name }}" == "push" ]]; then
echo "environment=Publish pkg.pr.new (maintainers)" >> "$GITHUB_OUTPUT"
elif [[ "${{ github.event.pull_request.head.repo.full_name }}" == "${{ github.repository }}" ]]; then
echo "environment=Publish pkg.pr.new (maintainers)" >> "$GITHUB_OUTPUT"
else
echo "environment=Publish pkg.pr.new (external contributors)" >> "$GITHUB_OUTPUT"
fi
build:
needs: resolve-env
runs-on: ubuntu-latest
# This is the line that ensures forks require manual approval before running the build job
environment: ${{ needs.resolve-env.outputs.environment }}
# No permissions — this job runs user-controlled code
permissions: {}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
# For pull_request_target, we must explicitly check out the PR head.
# This is safe because the environment gate above has already fired —
# an org member has approved this specific commit for external PRs.
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
- uses: actions/setup-node@v6
with:
node-version: 22.x
cache: pnpm
@ -24,21 +59,161 @@ jobs:
run: pnpm build
- 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
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const fs = require('fs');
const output = JSON.parse(fs.readFileSync('output.json', 'utf8'));
output.number = context.issue.number;
output.event_name = context.eventName;
output.ref = context.ref;
fs.writeFileSync('output.json', JSON.stringify(output), 'utf8');
- name: Upload output
uses: actions/upload-artifact@v4
with:
name: output
path: ./output.json
- run: ls -R .
# Sanitizes the untrusted output from the build job before it's consumed by
# jobs with elevated permissions. This ensures that only known package names
# and valid SHA prefixes make it through.
sanitize:
needs: build
runs-on: ubuntu-latest
permissions: {}
steps:
- name: Download artifact
uses: actions/download-artifact@v7
with:
name: output
- name: Sanitize output
uses: actions/github-script@v8
with:
script: |
const fs = require('fs');
const raw = JSON.parse(fs.readFileSync('output.json', 'utf8'));
const ALLOWED_PACKAGES = new Set(['svelte']);
const SHA_PATTERN = /^[0-9a-f]{7}$/;
const packages = (raw.packages || [])
.filter(p => {
if (!ALLOWED_PACKAGES.has(p.name)) {
console.log(`Skipping unexpected package: ${JSON.stringify(p.name)}`);
return false;
}
const sha = p.url?.replace(/^.+@([^@]+)$/, '$1');
if (!sha || !SHA_PATTERN.test(sha)) {
console.log(`Skipping package with invalid SHA: ${JSON.stringify(p.url)}`);
return false;
}
return true;
})
.map(p => ({
name: p.name,
sha: p.url.replace(/^.+@([^@]+)$/, '$1'),
}));
fs.writeFileSync('sanitized-output.json', JSON.stringify({ packages }), 'utf8');
- name: Upload sanitized output
uses: actions/upload-artifact@v4
with:
name: sanitized-output
path: ./sanitized-output.json
comment:
needs: sanitize
if: github.event_name == 'pull_request_target'
runs-on: ubuntu-latest
permissions:
pull-requests: write
steps:
- name: Download sanitized artifact
uses: actions/download-artifact@v7
with:
name: sanitized-output
- name: Post or update comment
uses: actions/github-script@v8
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const fs = require('fs');
const { packages } = JSON.parse(fs.readFileSync('sanitized-output.json', 'utf8'));
if (packages.length === 0) {
console.log('No valid packages found. Skipping comment.');
return;
}
// Issue number from trusted event context, never from the artifact
const issue_number = context.issue.number;
const bot_comment_identifier = `<!-- pkg.pr.new comment -->`;
const body = `${bot_comment_identifier}
[Playground](https://svelte.dev/playground?version=pr-${issue_number})
\`\`\`
${packages.map(p => `pnpm add https://pkg.pr.new/${p.name}@${issue_number}`).join('\n')}
\`\`\`
`;
const comments = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number,
});
const existing = comments.data.find(c => c.body.includes(bot_comment_identifier));
if (existing) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: existing.id,
body,
});
} else {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number,
body,
});
}
log:
needs: sanitize
if: github.event_name == 'push'
runs-on: ubuntu-latest
permissions: {}
steps:
- name: Download sanitized artifact
uses: actions/download-artifact@v7
with:
name: sanitized-output
- name: Log publish info
uses: actions/github-script@v8
with:
script: |
const fs = require('fs');
const { packages } = JSON.parse(fs.readFileSync('sanitized-output.json', 'utf8'));
if (packages.length === 0) {
console.log('No valid packages found.');
return;
}
console.log('\n' + '='.repeat(50));
console.log('Publish Information');
console.log('='.repeat(50));
for (const p of packages) {
console.log(`${p.name} - pnpm add https://pkg.pr.new/${p.name}@${p.sha}`);
}
const svelte = packages.find(p => p.name === 'svelte');
if (svelte) {
console.log(`\nPlayground: https://svelte.dev/playground?version=commit-${svelte.sha}`);
}
console.log('='.repeat(50));

@ -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:
@ -18,13 +23,13 @@ jobs:
runs-on: ubuntu-latest
steps:
- 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: 24.x
cache: pnpm

@ -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,7 +15,7 @@ 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](/packages#routing) 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).
@ -25,7 +25,7 @@ There are also [plugins for other bundlers](/packages#bundler-plugins), but we r
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 [`sv check`](https://github.com/sveltejs/cli).
You can also check your code from the command line using [`npx sv check`](https://svelte.dev/docs/cli/sv-check).
## Getting help

@ -1,5 +1,6 @@
---
title: $state
tags: rune-state
---
The `$state` rune allows you to create _reactive state_, which means that your UI _reacts_ when it changes.

@ -1,5 +1,6 @@
---
title: $derived
tags: rune-derived
---
Derived state is declared with the `$derived` rune:

@ -1,5 +1,6 @@
---
title: $effect
tags: rune-effect
---
Effects are functions that run when state updates, and can be used for things like calling third-party libraries, drawing on `<canvas>` elements, or making network requests. They only run in the browser, not during server-side rendering.

@ -1,5 +1,6 @@
---
title: $props
tags: rune-props
---
The inputs to a component are referred to as _props_, which is short for _properties_. You pass props to components just like you pass attributes to elements:
@ -198,6 +199,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.

@ -1,5 +1,6 @@
---
title: $inspect
tags: rune-inspect
---
> [!NOTE] `$inspect` only works during development. In a production build it becomes a noop.

@ -1,5 +1,6 @@
---
title: {#if ...}
tags: template-if
---
```svelte

@ -1,5 +1,6 @@
---
title: {#each ...}
tags: template-each
---
```svelte

@ -1,5 +1,6 @@
---
title: {#key ...}
tags: template-key
---
```svelte

@ -1,5 +1,6 @@
---
title: {#await ...}
tags: template-await
---
```svelte

@ -1,5 +1,6 @@
---
title: {@html ...}
tags: template-html
---
To inject raw HTML into your component, use the `{@html ...}` tag:

@ -1,5 +1,6 @@
---
title: {@attach ...}
tags: attachments
---
Attachments are functions that run in an [effect]($effect) when an element is mounted to the DOM or when [state]($state) read inside the function updates.
@ -82,6 +83,14 @@ Attachments can also be created inline ([demo](/playground/untitled#H4sIAAAAAAAA
> [!NOTE]
> The nested effect runs whenever `color` changes, while the outer effect (where `canvas.getContext(...)` is called) only runs once, since it doesn't read any reactive state.
## Conditional attachments
Falsy values like `false` or `undefined` are treated as no attachment, enabling conditional usage:
```svelte
<div {@attach enabled && myAttachment}>...</div>
```
## Passing attachments to components
When used on a component, `{@attach ...}` will create a prop whose key is a [`Symbol`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol). If the component then [spreads](/tutorial/svelte/spread-props) props onto an element, the element will receive those attachments.

@ -1,5 +1,6 @@
---
title: transition:
tags: transitions
---
A _transition_ is triggered by an element entering or leaving the DOM as a result of a state change.

@ -1,5 +1,6 @@
---
title: in: and out:
tags: transitions
---
The `in:` and `out:` directives are identical to [`transition:`](transition), except that the resulting transitions are not bidirectional — an `in` transition will continue to 'play' alongside the `out` transition, rather than reversing, if the block is outroed while the transition is in progress. If an out transition is aborted, transitions will restart from scratch.
@ -7,7 +8,7 @@ The `in:` and `out:` directives are identical to [`transition:`](transition), ex
```svelte
<script>
import { fade, fly } from 'svelte/transition';
let visible = $state(false);
</script>

@ -1,5 +1,6 @@
---
title: style:
tags: template-style
---
The `style:` directive provides a shorthand for setting multiple styles on an element.

@ -1,5 +1,6 @@
---
title: class
tags: template-style
---
There are two ways to set classes on elements: the `class` attribute, and the `class:` directive.

@ -1,5 +1,6 @@
---
title: Scoped styles
tags: styles-scoped
---
Svelte components can include a `<style>` element containing CSS that belongs to the component. This CSS is _scoped_ by default, meaning that styles will not apply to any elements on the page outside the component in question.

@ -1,5 +1,6 @@
---
title: Global styles
tags: styles-global
---
## :global(...)

@ -1,5 +1,6 @@
---
title: Custom properties
tags: styles-custom-properties
---
You can pass CSS custom properties — both static and dynamic — to components:

@ -97,6 +97,32 @@ import { createContext } from 'svelte';
export const [getUserContext, setUserContext] = createContext<User>();
```
When writing [component tests](testing#Unit-and-component-tests-with-Vitest-Component-testing), it can be useful to create a wrapper component that sets the context in order to check the behaviour of a component that uses it. As of version 5.49, you can do this sort of thing:
```js
import { mount, unmount } from 'svelte';
import { expect, test } from 'vitest';
import { setUserContext } from './context';
import MyComponent from './MyComponent.svelte';
test('MyComponent', () => {
function Wrapper(...args) {
setUserContext({ name: 'Bob' });
return MyComponent(...args);
}
const component = mount(Wrapper, {
target: document.body
});
expect(document.body.innerHTML).toBe('<h1>Hello Bob!</h1>');
unmount(component);
});
```
This approach also works with [`hydrate`](imperative-component-api#hydrate) and [`render`](imperative-component-api#render).
## Replacing global state
When you have state shared by many different components, you might be tempted to put it in its own module and just import it wherever it's needed:

@ -63,3 +63,61 @@ All data returned from a `hydratable` function must be serializable. But this do
{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.

@ -181,7 +181,7 @@ export default defineConfig({
/* ... */
],
test: {
// If you are testing components client-side, you need to setup a DOM environment.
// If you are testing components client-side, you need to set up a DOM environment.
// If not all your files should have this environment, you can use a
// `// @vitest-environment jsdom` comment at the top of the test files instead.
environment: 'jsdom'

@ -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' }
},

@ -778,9 +778,9 @@ In Svelte 4, doing the following triggered reactivity:
This is because the Svelte compiler treated the assignment to `foo.value` as an instruction to update anything that referenced `foo`. In Svelte 5, reactivity is determined at runtime rather than compile time, so you should define `value` as a reactive `$state` field on the `Foo` class. Wrapping `new Foo()` with `$state(...)` will have no effect — only vanilla objects and arrays are made deeply reactive.
### Touch and wheel events are passive
### Touch events are passive
When using `onwheel`, `onmousewheel`, `ontouchstart` and `ontouchmove` event attributes, the handlers are [passive](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#using_passive_listeners) to align with browser defaults. This greatly improves responsiveness by allowing the browser to scroll the document immediately, rather than waiting to see if the event handler calls `event.preventDefault()`.
When using `ontouchstart` and `ontouchmove` event attributes, the handlers are [passive](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#using_passive_listeners) to align with browser defaults. This greatly improves responsiveness by allowing the browser to scroll the document immediately, rather than waiting to see if the event handler calls `event.preventDefault()`.
In the very rare cases that you need to prevent these event defaults, you should use [`on`](/docs/svelte/svelte-events#on) instead (for example inside an action).

@ -91,7 +91,7 @@ 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 and Nuxt.js for Vue.
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 sampling of available routers are highlighted [on the packages page](/packages#routing).

@ -193,7 +193,7 @@ 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
@ -1102,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
@ -1114,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
```

@ -55,6 +55,12 @@ Cause:
%stack%
```
### invalid_csp
```
`csp.nonce` was set while `csp.hash` was `true`. These options cannot be used simultaneously.
```
### lifecycle_function_unavailable
```

@ -1,5 +1,6 @@
---
title: svelte/attachments
tags: attachments
---
> MODULE: svelte/attachments

@ -1,5 +1,6 @@
---
title: svelte/transition
tags: transitions
---
> MODULE: svelte/transition

@ -33,7 +33,7 @@ const no_compiler_imports = {
}
};
/** @type {import('eslint').Linter.FlatConfig[]} */
/** @type {import('eslint').Linter.Config[]} */
export default [
...svelte_config,
{
@ -102,6 +102,7 @@ export default [
'playgrounds/sandbox/**',
// exclude top level config files
'*.config.js',
'vitest-xhtml-environment.ts',
// documentation can contain invalid examples
'documentation',
'tmp/**'

@ -21,28 +21,36 @@
"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.29.8",
"@sveltejs/eslint-config": "^8.3.3",
"@sveltejs/eslint-config": "^8.3.5",
"@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",
"eslint-plugin-svelte": "^3.11.0",
"@eslint/js": "^10.0.0",
"eslint": "^10.0.0",
"eslint-plugin-lube": "^0.5.1",
"eslint-plugin-svelte": "^3.15.0",
"jsdom": "25.0.1",
"playwright": "^1.46.1",
"playwright": "^1.58.0",
"prettier": "^3.2.4",
"prettier-plugin-svelte": "^3.4.0",
"svelte": "workspace:^",
"typescript": "^5.5.4",
"typescript-eslint": "^8.48.1",
"typescript-eslint": "^8.55.0",
"v8-natives": "^1.2.5",
"vitest": "^2.1.9"
},
"pnpm": {
"peerDependencyRules": {
"allowedVersions": {
"eslint": "10"
}
}
}
}

@ -1,5 +1,233 @@
# svelte
## 5.50.1
### Patch Changes
- fix: render boolean attribute values as empty strings for XHTML compliance ([#17648](https://github.com/sveltejs/svelte/pull/17648))
- fix: prevent async render tag hydration mismatches ([#17652](https://github.com/sveltejs/svelte/pull/17652))
## 5.50.0
### Minor Changes
- feat: allow use of createContext when instantiating components programmatically ([#17575](https://github.com/sveltejs/svelte/pull/17575))
### Patch Changes
- fix: ensure infinite effect loops are cleared after flushing ([#17601](https://github.com/sveltejs/svelte/pull/17601))
- fix: allow `{#key NaN}` ([#17642](https://github.com/sveltejs/svelte/pull/17642))
- fix: detect store in each block expression regardless of AST shape ([#17636](https://github.com/sveltejs/svelte/pull/17636))
- fix: treat `<menu>` like `<ul>`/`<ol>` for a11y role checks ([#17638](https://github.com/sveltejs/svelte/pull/17638))
- fix: add vite-ignore comment inside dynamic crypto import ([#17623](https://github.com/sveltejs/svelte/pull/17623))
- chore: wrap JSDoc URLs in `@see` and `@link` tags ([#17617](https://github.com/sveltejs/svelte/pull/17617))
- fix: properly hydrate already-resolved async blocks ([#17641](https://github.com/sveltejs/svelte/pull/17641))
- fix: emit `each_key_duplicate` error in production ([#16724](https://github.com/sveltejs/svelte/pull/16724))
- fix: exit resolved async blocks on correct node when hydrating ([#17640](https://github.com/sveltejs/svelte/pull/17640))
## 5.49.2
### Patch Changes
- chore: remove SvelteKit data attributes from elements.d.ts ([#17613](https://github.com/sveltejs/svelte/pull/17613))
- fix: avoid erroneous async derived expressions for blocks ([#17604](https://github.com/sveltejs/svelte/pull/17604))
- fix: avoid Cloudflare warnings about not having the "node:crypto" module ([#17612](https://github.com/sveltejs/svelte/pull/17612))
- fix: reschedule effects inside unskipped branches ([#17604](https://github.com/sveltejs/svelte/pull/17604))
## 5.49.1
### Patch Changes
- fix: merge consecutive large text nodes ([#17587](https://github.com/sveltejs/svelte/pull/17587))
- fix: only create async functions in SSR output when necessary ([#17593](https://github.com/sveltejs/svelte/pull/17593))
- fix: properly separate multiline html blocks from each other in `print()` ([#17319](https://github.com/sveltejs/svelte/pull/17319))
- fix: prevent unhandled exceptions arising from dangling promises in <script> ([#17591](https://github.com/sveltejs/svelte/pull/17591))
## 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

@ -851,23 +851,6 @@ export interface HTMLAttributes<T extends EventTarget> extends AriaAttributes, D
readonly 'bind:offsetWidth'?: number | undefined | null;
readonly 'bind:offsetHeight'?: number | undefined | null;
// SvelteKit
'data-sveltekit-keepfocus'?: true | '' | 'off' | undefined | null;
'data-sveltekit-noscroll'?: true | '' | 'off' | undefined | null;
'data-sveltekit-preload-code'?:
| true
| ''
| 'eager'
| 'viewport'
| 'hover'
| 'tap'
| 'off'
| undefined
| null;
'data-sveltekit-preload-data'?: true | '' | 'hover' | 'tap' | 'off' | undefined | null;
'data-sveltekit-reload'?: true | '' | 'off' | undefined | null;
'data-sveltekit-replacestate'?: true | '' | 'off' | undefined | null;
// allow any data- attribute
[key: `data-${string}`]: any;
@ -2062,7 +2045,7 @@ export interface SvelteHTMLElements {
| undefined
| {
tag?: string;
shadow?: 'open' | 'none' | undefined;
shadow?: 'open' | 'none' | ShadowRootInit | undefined;
props?:
| Record<
string,

@ -122,7 +122,7 @@
## 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
@ -411,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
@ -419,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

@ -43,6 +43,10 @@ This error occurs when using `hydratable` multiple times with the same key. To a
> 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

@ -2,7 +2,7 @@
"name": "svelte",
"description": "Cybernetically enhanced web apps",
"license": "MIT",
"version": "5.45.8",
"version": "5.50.1",
"type": "module",
"types": "./types/index.d.ts",
"engines": {
@ -150,7 +150,7 @@
},
"devDependencies": {
"@jridgewell/trace-mapping": "^0.3.25",
"@playwright/test": "^1.46.1",
"@playwright/test": "^1.58.0",
"@rollup/plugin-commonjs": "^28.0.1",
"@rollup/plugin-node-resolve": "^15.3.0",
"@rollup/plugin-terser": "^0.4.4",
@ -174,9 +174,9 @@
"aria-query": "^5.3.1",
"axobject-query": "^4.1.0",
"clsx": "^2.1.1",
"devalue": "^5.5.0",
"devalue": "^5.6.2",
"esm-env": "^1.2.1",
"esrap": "^2.2.1",
"esrap": "^2.2.2",
"is-reference": "^3.0.3",
"locate-character": "^3.0.0",
"magic-string": "^0.30.11",

@ -16,7 +16,7 @@ declare module '*.svelte' {
* let count = $state(0);
* ```
*
* https://svelte.dev/docs/svelte/$state
* @see {@link https://svelte.dev/docs/svelte/$state Documentation}
*
* @param initial The initial value
*/
@ -126,7 +126,7 @@ declare namespace $state {
* </button>
* ```
*
* https://svelte.dev/docs/svelte/$state#$state.raw
* @see {@link https://svelte.dev/docs/svelte/$state#$state.raw Documentation}
*
* @param initial The initial value
*/
@ -147,7 +147,7 @@ declare namespace $state {
* </script>
* ```
*
* https://svelte.dev/docs/svelte/$state#$state.snapshot
* @see {@link https://svelte.dev/docs/svelte/$state#$state.snapshot Documentation}
*
* @param state The value to snapshot
*/
@ -173,6 +173,9 @@ declare namespace $state {
export const prototype: never;
/** @deprecated */
export const toString: never;
// needed to keep private stuff private
export {};
}
/**
@ -184,7 +187,7 @@ declare namespace $state {
* let double = $derived(count * 2);
* ```
*
* https://svelte.dev/docs/svelte/$derived
* @see {@link https://svelte.dev/docs/svelte/$derived Documentation}
*
* @param expression The derived state expression
*/
@ -206,7 +209,7 @@ declare namespace $derived {
* });
* ```
*
* https://svelte.dev/docs/svelte/$derived#$derived.by
* @see {@link https://svelte.dev/docs/svelte/$derived#$derived.by Documentation}
*/
export function by<T>(fn: () => T): T;
@ -230,6 +233,9 @@ declare namespace $derived {
export const prototype: never;
/** @deprecated */
export const toString: never;
// needed to keep private stuff private
export {};
}
/**
@ -245,7 +251,7 @@ declare namespace $derived {
*
* Does not run during server-side rendering.
*
* https://svelte.dev/docs/svelte/$effect
* @see {@link https://svelte.dev/docs/svelte/$effect Documentation}
* @param fn The function to execute
*/
declare function $effect(fn: () => void | (() => void)): void;
@ -264,7 +270,7 @@ declare namespace $effect {
*
* Does not run during server-side rendering.
*
* https://svelte.dev/docs/svelte/$effect#$effect.pre
* @see {@link https://svelte.dev/docs/svelte/$effect#$effect.pre Documentation}
* @param fn The function to execute
*/
export function pre(fn: () => void | (() => void)): void;
@ -272,7 +278,7 @@ declare namespace $effect {
/**
* Returns the number of promises that are pending in the current boundary, not including child boundaries.
*
* https://svelte.dev/docs/svelte/$effect#$effect.pending
* @see {@link https://svelte.dev/docs/svelte/$effect#$effect.pending Documentation}
*/
export function pending(): number;
@ -294,7 +300,7 @@ declare namespace $effect {
*
* This allows you to (for example) add things like subscriptions without causing memory leaks, by putting them in child effects.
*
* https://svelte.dev/docs/svelte/$effect#$effect.tracking
* @see {@link https://svelte.dev/docs/svelte/$effect#$effect.tracking Documentation}
*/
export function tracking(): boolean;
@ -322,7 +328,7 @@ declare namespace $effect {
* <button onclick={() => cleanup()}>cleanup</button>
* ```
*
* https://svelte.dev/docs/svelte/$effect#$effect.root
* @see {@link https://svelte.dev/docs/svelte/$effect#$effect.root Documentation}
*/
export function root(fn: () => void | (() => void)): () => void;
@ -346,6 +352,9 @@ declare namespace $effect {
export const prototype: never;
/** @deprecated */
export const toString: never;
// needed to keep private stuff private
export {};
}
/**
@ -355,7 +364,7 @@ declare namespace $effect {
* let { optionalProp = 42, requiredProp, bindableProp = $bindable() }: { optionalProp?: number; requiredProps: string; bindableProp: boolean } = $props();
* ```
*
* https://svelte.dev/docs/svelte/$props
* @see {@link https://svelte.dev/docs/svelte/$props Documentation}
*/
declare function $props(): any;
@ -389,6 +398,9 @@ declare namespace $props {
export const prototype: never;
/** @deprecated */
export const toString: never;
// needed to keep private stuff private
export {};
}
/**
@ -398,7 +410,7 @@ declare namespace $props {
* let { propName = $bindable() }: { propName: boolean } = $props();
* ```
*
* https://svelte.dev/docs/svelte/$bindable
* @see {@link https://svelte.dev/docs/svelte/$bindable Documentation}
*/
declare function $bindable<T>(fallback?: T): T;
@ -423,6 +435,9 @@ declare namespace $bindable {
export const prototype: never;
/** @deprecated */
export const toString: never;
// needed to keep private stuff private
export {};
}
/**
@ -441,7 +456,7 @@ declare namespace $bindable {
* $inspect(x, y).with(() => { debugger; });
* ```
*
* https://svelte.dev/docs/svelte/$inspect
* @see {@link https://svelte.dev/docs/svelte/$inspect Documentation}
*/
declare function $inspect<T extends any[]>(
...values: T
@ -485,6 +500,9 @@ declare namespace $inspect {
export const prototype: never;
/** @deprecated */
export const toString: never;
// needed to keep private stuff private
export {};
}
/**
@ -504,7 +522,7 @@ declare namespace $inspect {
*
* Only available inside custom element components, and only on the client-side.
*
* https://svelte.dev/docs/svelte/$host
* @see {@link https://svelte.dev/docs/svelte/$host Documentation}
*/
declare function $host<El extends HTMLElement = HTMLElement>(): El;
@ -529,4 +547,7 @@ declare namespace $host {
export const prototype: never;
/** @deprecated */
export const toString: never;
// needed to keep private stuff private
export {};
}

@ -977,12 +977,12 @@ 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`);
}
/**
@ -1550,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`);
}
/**
@ -1568,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,8 +3,9 @@
/** @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';
@ -118,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

@ -34,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;

@ -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 === ')') {
@ -569,7 +568,7 @@ function read_attribute_value(parser) {
}
/**
* https://www.w3.org/TR/css-syntax-3/#ident-token-diagram
* @see {@link https://www.w3.org/TR/css-syntax-3/#ident-token-diagram CSS Syntax Module Level 3}
* @param {Parser} parser
*/
function read_identifier(parser) {
@ -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);
}

@ -46,10 +46,11 @@ function levenshtein(str1, str2) {
/** @type {number[]} */
const current = [];
let prev = 0;
let value = 0;
for (let i = 0; i <= str2.length; i++) {
for (let j = 0; j <= str1.length; j++) {
let value;
if (i && j) {
if (str1.charAt(j - 1) === str2.charAt(i - 1)) {
value = prev;

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

@ -657,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;

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

@ -24,4 +24,23 @@ export function IfBlock(node, context) {
context.visit(node.consequent);
if (node.alternate) context.visit(node.alternate);
// Check if we can flatten branches
const alt = node.alternate;
if (alt && alt.nodes.length === 1 && alt.nodes[0].type === 'IfBlock' && alt.nodes[0].elseif) {
const elseif = alt.nodes[0];
// Don't flatten if this else-if has an await expression or new blockers
// TODO would be nice to check the await expression itself to see if it's awaiting the same thing as a previous if expression
if (
!elseif.metadata.expression.has_await &&
!elseif.metadata.expression.has_more_blockers_than(node.metadata.expression)
) {
// Roll the existing flattened branches (if any) into this one, then delete those of the else-if block
// to avoid processing them multiple times as we walk down the chain during code transformation.
node.metadata.flattened = [elseif, ...(elseif.metadata.flattened ?? [])];
elseif.metadata.flattened = undefined;
}
}
}

@ -7,7 +7,11 @@ import {
} from '../../../../html-tree-validation.js';
import * as e from '../../../errors.js';
import * as w from '../../../warnings.js';
import { create_attribute, is_custom_element_node } from '../../nodes.js';
import {
create_attribute,
is_custom_element_node,
is_customizable_select_element
} from '../../nodes.js';
import { regex_starts_with_newline } from '../../patterns.js';
import { check_element } from './shared/a11y/index.js';
import { validate_element } from './shared/element.js';
@ -74,6 +78,15 @@ export function RegularElement(node, context) {
node.metadata.synthetic_value_node = child;
}
// Special case: <select>, <option> or <optgroup> with rich content needs special hydration handling
// We mark the subtree as dynamic so parent elements properly include the child init code
if (is_customizable_select_element(node) || node.name === 'selectedcontent') {
// Mark the element's own fragment as dynamic so it's not treated as static
node.fragment.metadata.dynamic = true;
// Also mark ancestor fragments so parents properly include the child init code
mark_subtree_dynamic(context.path);
}
const binding = context.state.scope.get(node.name);
if (
binding !== null &&

@ -1,6 +1,7 @@
/** @import { AST } from '#compiler' */
/** @import { Context } from '../types' */
import * as e from '../../../errors.js';
import { mark_subtree_dynamic } from './shared/fragment.js';
const valid = ['onerror', 'failed', 'pending'];
@ -23,5 +24,7 @@ export function SvelteBoundary(node, context) {
}
}
mark_subtree_dynamic(context.path);
context.next();
}

@ -67,7 +67,21 @@ export const a11y_interactive_handlers = [
'mousemove',
'mouseout',
'mouseover',
'mouseup'
'mouseup',
// Pointer events
'pointerdown',
'pointerup',
'pointermove',
'pointerenter',
'pointerleave',
'pointerover',
'pointerout',
'pointercancel',
// Touch events
'touchstart',
'touchend',
'touchmove',
'touchcancel'
];
export const a11y_recommended_interactive_handlers = [
@ -160,6 +174,7 @@ export const input_type_to_implicit_role = new Map([
export const a11y_non_interactive_element_to_interactive_role_exceptions = {
ul: ['listbox', 'menu', 'menubar', 'radiogroup', 'tablist', 'tree', 'treegrid'],
ol: ['listbox', 'menu', 'menubar', 'radiogroup', 'tablist', 'tree', 'treegrid'],
menu: ['listbox', 'menu', 'menubar', 'radiogroup', 'tablist', 'tree', 'treegrid'],
li: ['menuitem', 'option', 'row', 'tab', 'treeitem'],
table: ['grid'],
td: ['gridcell'],

@ -167,7 +167,7 @@ export function check_element(node, context) {
if (
current_role === get_implicit_role(node.name, attribute_map) &&
// <ul role="list"> is ok because CSS list-style:none removes the semantics and this is a way to bring them back
!['ul', 'ol', 'li'].includes(node.name) &&
!['ul', 'ol', 'li', 'menu'].includes(node.name) &&
// <a role="link" /> is ok because without href the a tag doesn't have a role of link
!(node.name === 'a' && !attribute_map.has('href'))
) {
@ -824,6 +824,10 @@ function has_content(element) {
}
if (node.type === 'RegularElement' || node.type === 'SvelteElement') {
if (node.attributes.some((a) => a.type === 'Attribute' && a.name === 'popover')) {
continue;
}
if (
node.name === 'img' &&
node.attributes.some((node) => node.type === 'Attribute' && node.name === 'alt')
@ -831,6 +835,10 @@ function has_content(element) {
return true;
}
if (node.name === 'selectedcontent') {
return true;
}
if (!has_content(node)) {
continue;
}

@ -166,10 +166,12 @@ export function client_component(analysis, options) {
in_constructor: false,
instance_level_snippets: [],
module_level_snippets: [],
is_standalone: false,
// these are set inside the `Fragment` visitor, and cannot be used until then
init: /** @type {any} */ (null),
consts: /** @type {any} */ (null),
snippets: /** @type {any} */ (null),
let_directives: /** @type {any} */ (null),
update: /** @type {any} */ (null),
after_update: /** @type {any} */ (null),
@ -519,14 +521,9 @@ export function client_component(analysis, options) {
if (options.hmr) {
const id = b.id(analysis.name);
const HMR = b.id('$.HMR');
const existing = b.member(id, HMR, true);
const incoming = b.member(b.id('module.default'), HMR, true);
const accept_fn_body = [
b.stmt(b.assignment('=', b.member(incoming, 'source'), b.member(existing, 'source'))),
b.stmt(b.call('$.set', b.member(existing, 'source'), b.member(incoming, 'original')))
b.stmt(b.call(b.member(b.member(id, b.id('$.HMR'), true), 'update'), b.id('module.default')))
];
if (analysis.css.hash) {
@ -535,8 +532,7 @@ export function client_component(analysis, options) {
}
const hmr = b.block([
b.stmt(b.assignment('=', id, b.call('$.hmr', id, b.thunk(b.member(existing, 'source'))))),
b.stmt(b.assignment('=', id, b.call('$.hmr', id))),
b.stmt(b.call('import.meta.hot.accept', b.arrow([b.id('module')], b.block(accept_fn_body))))
]);
@ -643,7 +639,16 @@ export function client_component(analysis, options) {
const accessors_str = b.array(
analysis.exports.map(({ name, alias }) => b.literal(alias ?? name))
);
const use_shadow_dom = typeof ce === 'boolean' || ce.shadow !== 'none' ? true : false;
/** @type {ESTree.ObjectExpression | undefined} */
let shadow_root_init;
if (typeof ce === 'boolean' || ce.shadow === 'open' || ce.shadow === undefined) {
shadow_root_init = b.object([b.init('mode', b.literal('open'))]);
} else if (ce.shadow === 'none') {
shadow_root_init = undefined;
} else {
shadow_root_init = ce.shadow;
}
const create_ce = b.call(
'$.create_custom_element',
@ -651,7 +656,7 @@ export function client_component(analysis, options) {
b.object(props_str),
slots_str,
accessors_str,
b.literal(use_shadow_dom),
shadow_root_init,
/** @type {any} */ (typeof ce !== 'boolean' ? ce.extend : undefined)
);

@ -49,6 +49,8 @@ export interface ComponentClientTransformState extends ClientTransformState {
readonly update: Statement[];
/** Stuff that happens after the render effect (control blocks, dynamic elements, bindings, actions, etc) */
readonly after_update: Statement[];
/** Transformed `{#snippets }` declarations */
readonly snippets: Statement[];
/** Transformed `{@const }` declarations */
readonly consts: Statement[];
/** Transformed async `{@const }` declarations (if any) and those coming after them */
@ -81,6 +83,9 @@ export interface ComponentClientTransformState extends ClientTransformState {
readonly instance_level_snippets: VariableDeclaration[];
/** Snippets hoisted to the module */
readonly module_level_snippets: VariableDeclaration[];
/** True if the current node is a) a component or render tag and b) the sole child of a block */
readonly is_standalone: boolean;
}
export type Context = import('zimmerframe').Context<AST.SvelteNode, ClientTransformState>;

@ -162,7 +162,10 @@ function build_assignment(operator, left, right, context) {
// will be pushed to. we do this by transforming it to something like
// `$.assign_nullish(object, 'items', [])`
let should_transform =
dev && path.at(-1) !== 'ExpressionStatement' && is_non_coercive_operator(operator);
dev &&
path.at(-1) !== 'ExpressionStatement' &&
is_non_coercive_operator(operator) &&
!context.state.scope.evaluate(right).is_primitive;
// special case — ignore `onclick={() => (...)}`
if (

@ -1,4 +1,4 @@
/** @import { Pattern } from 'estree' */
/** @import { Expression, Identifier, Pattern } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */
/** @import { ExpressionMetadata } from '../../../nodes.js' */
@ -88,8 +88,8 @@ export function ConstTag(node, context) {
/**
* @param {ComponentContext['state']} state
* @param {import('estree').Identifier} id
* @param {import('estree').Expression} expression
* @param {Identifier} id
* @param {Expression} expression
* @param {ExpressionMetadata} metadata
* @param {import('#compiler').Binding[]} bindings
*/
@ -99,7 +99,9 @@ function add_const_declaration(state, id, expression, metadata, bindings) {
const after = dev ? [b.stmt(b.call('$.get', id))] : [];
const has_await = metadata.has_await;
const blockers = [...metadata.dependencies].map((dep) => dep.blocker).filter((b) => b !== null);
const blockers = [...metadata.dependencies]
.map((dep) => dep.blocker)
.filter((b) => b !== null && b.object !== state.async_consts?.id);
if (has_await || state.async_consts || blockers.length > 0) {
const run = (state.async_consts ??= {
@ -112,7 +114,11 @@ function add_const_declaration(state, id, expression, metadata, bindings) {
const assignment = b.assignment('=', id, expression);
const body = after.length === 0 ? assignment : b.block([b.stmt(assignment), ...after]);
if (blockers.length > 0) run.thunks.push(b.thunk(b.call('Promise.all', b.array(blockers))));
if (blockers.length === 1) {
run.thunks.push(b.thunk(b.member(/** @type {Expression} */ (blockers[0]), 'promise')));
} else if (blockers.length > 0) {
run.thunks.push(b.thunk(b.call('$.wait', b.array(blockers))));
}
run.thunks.push(b.thunk(body, has_await));

@ -101,15 +101,11 @@ export function EachBlock(node, context) {
}
// If the array is a store expression, we need to invalidate it when the array is changed.
// This doesn't catch all cases, but all the ones that Svelte 4 catches, too.
let store_to_invalidate = '';
if (node.expression.type === 'Identifier' || node.expression.type === 'MemberExpression') {
const id = object(node.expression);
if (id) {
const binding = context.state.scope.get(id.name);
if (binding?.kind === 'store_sub') {
store_to_invalidate = id.name;
}
for (const binding of node.metadata.expression.dependencies) {
if (binding.kind === 'store_sub') {
store_to_invalidate = binding.node.name;
break;
}
}
@ -312,10 +308,10 @@ export function EachBlock(node, context) {
declarations.push(b.let(node.index, index));
}
const is_async = node.metadata.expression.is_async();
const has_await = node.metadata.expression.has_await;
const get_collection = b.thunk(collection, node.metadata.expression.has_await);
const thunk = is_async ? b.thunk(b.call('$.get', b.id('$$collection'))) : get_collection;
const get_collection = b.thunk(collection, has_await);
const thunk = has_await ? b.thunk(b.call('$.get', b.id('$$collection'))) : get_collection;
const render_args = [b.id('$$anchor'), item];
if (uses_index || collection_id) render_args.push(index);
@ -338,19 +334,18 @@ export function EachBlock(node, context) {
const statements = [add_svelte_meta(b.call('$.each', ...args), node, 'each')];
if (dev && node.metadata.keyed) {
statements.unshift(b.stmt(b.call('$.validate_each_keys', thunk, key_function)));
}
if (is_async) {
if (node.metadata.expression.is_async()) {
context.state.init.push(
b.stmt(
b.call(
'$.async',
context.state.node,
node.metadata.expression.blockers(),
b.array([get_collection]),
b.arrow([context.state.node, b.id('$$collection')], b.block(statements))
has_await ? b.array([get_collection]) : b.void0,
b.arrow(
has_await ? [context.state.node, b.id('$$collection')] : [context.state.node],
b.block(statements)
)
)
)
);

@ -1,13 +1,13 @@
/** @import { Expression, Statement } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentClientTransformState, ComponentContext } from '../types' */
import { TEMPLATE_FRAGMENT, TEMPLATE_USE_IMPORT_NODE } from '../../../../../constants.js';
import * as b from '#compiler/builders';
import { TEMPLATE_FRAGMENT, TEMPLATE_USE_IMPORT_NODE } from '../../../../../constants.js';
import { clean_nodes, infer_namespace } from '../../utils.js';
import { transform_template } from '../transform-template/index.js';
import { Template } from '../transform-template/template.js';
import { process_children } from './shared/fragment.js';
import { build_render_statement, Memoizer } from './shared/utils.js';
import { Template } from '../transform-template/template.js';
/**
* @param {AST.Fragment} node
@ -47,7 +47,11 @@ export function Fragment(node, context) {
const is_single_element = trimmed.length === 1 && trimmed[0].type === 'RegularElement';
const is_single_child_not_needing_template =
trimmed.length === 1 &&
(trimmed[0].type === 'SvelteFragment' || trimmed[0].type === 'TitleElement');
(trimmed[0].type === 'SvelteFragment' ||
trimmed[0].type === 'TitleElement' ||
(trimmed[0].type === 'IfBlock' &&
trimmed[0].elseif &&
/** @type {AST.IfBlock} */ (parent).metadata.flattened?.includes(trimmed[0])));
const template_name = context.state.scope.root.unique('root'); // TODO infer name from parent
/** @type {Statement[]} */
@ -60,6 +64,7 @@ export function Fragment(node, context) {
const state = {
...context.state,
init: [],
snippets: [],
consts: [],
let_directives: [],
update: [],
@ -119,38 +124,39 @@ export function Fragment(node, context) {
state.init.unshift(b.var(id, b.call('$.text')));
close = b.stmt(b.call('$.append', b.id('$$anchor'), id));
} else if (is_standalone) {
// no need to create a template, we can just use the existing block's anchor
process_children(trimmed, () => b.id('$$anchor'), false, {
...context,
state: { ...state, is_standalone }
});
} else {
if (is_standalone) {
// no need to create a template, we can just use the existing block's anchor
process_children(trimmed, () => b.id('$$anchor'), false, { ...context, state });
} else {
/** @type {(is_text: boolean) => Expression} */
const expression = (is_text) => b.call('$.first_child', id, is_text && b.true);
process_children(trimmed, expression, false, { ...context, state });
/** @type {(is_text: boolean) => Expression} */
const expression = (is_text) => b.call('$.first_child', id, is_text && b.true);
let flags = TEMPLATE_FRAGMENT;
process_children(trimmed, expression, false, { ...context, state });
if (state.template.needs_import_node) {
flags |= TEMPLATE_USE_IMPORT_NODE;
}
let flags = TEMPLATE_FRAGMENT;
if (state.template.nodes.length === 1 && state.template.nodes[0].type === 'comment') {
// special case — we can use `$.comment` instead of creating a unique template
state.init.unshift(b.var(id, b.call('$.comment')));
} else {
const template = transform_template(state, namespace, flags);
state.hoisted.push(b.var(template_name, template));
if (state.template.needs_import_node) {
flags |= TEMPLATE_USE_IMPORT_NODE;
}
state.init.unshift(b.var(id, b.call(template_name)));
}
if (state.template.nodes.length === 1 && state.template.nodes[0].type === 'comment') {
// special case — we can use `$.comment` instead of creating a unique template
state.init.unshift(b.var(id, b.call('$.comment')));
} else {
const template = transform_template(state, namespace, flags);
state.hoisted.push(b.var(template_name, template));
close = b.stmt(b.call('$.append', b.id('$$anchor'), id));
state.init.unshift(b.var(id, b.call(template_name)));
}
close = b.stmt(b.call('$.append', b.id('$$anchor'), id));
}
}
body.push(...state.let_directives, ...state.consts);
body.push(...state.snippets, ...state.let_directives, ...state.consts);
if (state.async_consts && state.async_consts.thunks.length > 0) {
body.push(b.var(state.async_consts.id, b.call('$.run', b.array(state.async_consts.thunks))));

@ -11,10 +11,11 @@ import { build_expression } from './shared/utils.js';
export function HtmlTag(node, context) {
context.state.template.push_comment();
const is_async = node.metadata.expression.is_async();
const has_await = node.metadata.expression.has_await;
const has_blockers = node.metadata.expression.has_blockers();
const expression = build_expression(context, node.expression, node.metadata.expression);
const html = is_async ? b.call('$.get', b.id('$$html')) : expression;
const html = has_await ? b.call('$.get', b.id('$$html')) : expression;
const is_svg = context.state.metadata.namespace === 'svg';
const is_mathml = context.state.metadata.namespace === 'mathml';
@ -31,15 +32,18 @@ export function HtmlTag(node, context) {
);
// push into init, so that bindings run afterwards, which might trigger another run and override hydration
if (is_async) {
if (has_await || has_blockers) {
context.state.init.push(
b.stmt(
b.call(
'$.async',
context.state.node,
node.metadata.expression.blockers(),
b.array([b.thunk(expression, node.metadata.expression.has_await)]),
b.arrow([context.state.node, b.id('$$html')], b.block([statement]))
has_await ? b.array([b.thunk(expression, true)]) : b.void0,
b.arrow(
has_await ? [context.state.node, b.id('$$html')] : [context.state.node],
b.block([statement])
)
)
)
);

@ -1,4 +1,4 @@
/** @import { BlockStatement, Expression } from 'estree' */
/** @import { BlockStatement, Expression, IfStatement, Statement } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */
import * as b from '#compiler/builders';
@ -10,39 +10,75 @@ import { build_expression, add_svelte_meta } from './shared/utils.js';
*/
export function IfBlock(node, context) {
context.state.template.push_comment();
/** @type {Statement[]} */
const statements = [];
const consequent = /** @type {BlockStatement} */ (context.visit(node.consequent));
const consequent_id = b.id(context.state.scope.generate('consequent'));
const has_await = node.metadata.expression.has_await;
const has_blockers = node.metadata.expression.has_blockers();
const expression = build_expression(context, node.test, node.metadata.expression);
statements.push(b.var(consequent_id, b.arrow([b.id('$$anchor')], consequent)));
// Build the if/else-if/else chain
let index = 0;
/** @type {IfStatement | undefined} */
let first_if;
/** @type {IfStatement | undefined} */
let last_if;
/** @type {AST.IfBlock | undefined} */
let last_alt;
let alternate_id;
for (const branch of [node, ...(node.metadata.flattened ?? [])]) {
const consequent = /** @type {BlockStatement} */ (context.visit(branch.consequent));
const consequent_id = b.id(context.state.scope.generate('consequent'));
statements.push(b.var(consequent_id, b.arrow([b.id('$$anchor')], consequent)));
if (node.alternate) {
const alternate = /** @type {BlockStatement} */ (context.visit(node.alternate));
alternate_id = b.id(context.state.scope.generate('alternate'));
statements.push(b.var(alternate_id, b.arrow([b.id('$$anchor')], alternate)));
// Build the test expression for this branch
/** @type {Expression} */
let test;
if (branch.metadata.expression.has_await) {
// Top-level condition with await: already resolved by $.async wrapper
test = b.call('$.get', b.id('$$condition'));
} else {
const expression = build_expression(context, branch.test, branch.metadata.expression);
if (branch.metadata.expression.has_call) {
const derived_id = b.id(context.state.scope.generate('d'));
statements.push(b.var(derived_id, b.call('$.derived', b.arrow([], expression))));
test = b.call('$.get', derived_id);
} else {
test = expression;
}
}
const render_call = b.stmt(b.call('$$render', consequent_id, index > 0 && b.literal(index)));
const new_if = b.if(test, render_call);
if (last_if) {
last_if.alternate = new_if;
} else {
first_if = new_if;
}
last_alt = branch;
last_if = new_if;
index++;
}
const is_async = node.metadata.expression.is_async();
// Handle final alternate (else branch, remaining async chain, or nothing)
if (last_if && last_alt?.alternate) {
const alternate = /** @type {BlockStatement} */ (context.visit(last_alt.alternate));
const alternate_id = b.id(context.state.scope.generate('alternate'));
statements.push(b.var(alternate_id, b.arrow([b.id('$$anchor')], alternate)));
const expression = build_expression(context, node.test, node.metadata.expression);
const test = is_async ? b.call('$.get', b.id('$$condition')) : expression;
last_if.alternate = b.stmt(b.call('$$render', alternate_id, b.literal(false)));
}
// Build $.if() arguments
/** @type {Expression[]} */
const args = [
context.state.node,
b.arrow(
[b.id('$$render')],
b.block([
b.if(
test,
b.stmt(b.call('$$render', consequent_id)),
alternate_id && b.stmt(b.call('$$render', alternate_id, b.literal(false)))
)
])
)
b.arrow([b.id('$$render')], first_if ? b.block([first_if]) : b.block([]))
];
if (node.elseif) {
@ -66,21 +102,26 @@ export function IfBlock(node, context) {
//
// ...even though they're logically equivalent. In the first case, the
// transition will only play when `y` changes, but in the second it
// should play when `x` or `y` change — both are considered 'local'
// should play when `x` or `y` change — both are considered 'local'.
// This could also be a non-flattened elseif (because it has an async expression).
// In both cases mark as elseif so the runtime uses EFFECT_TRANSPARENT for transitions.
args.push(b.true);
}
statements.push(add_svelte_meta(b.call('$.if', ...args), node, 'if'));
if (is_async) {
if (has_await || has_blockers) {
context.state.init.push(
b.stmt(
b.call(
'$.async',
context.state.node,
node.metadata.expression.blockers(),
b.array([b.thunk(expression, node.metadata.expression.has_await)]),
b.arrow([context.state.node, b.id('$$condition')], b.block(statements))
has_await ? b.array([b.thunk(expression, true)]) : b.void0,
b.arrow(
has_await ? [context.state.node, b.id('$$condition')] : [context.state.node],
b.block(statements)
)
)
)
);

@ -11,29 +11,35 @@ import { build_expression, add_svelte_meta } from './shared/utils.js';
export function KeyBlock(node, context) {
context.state.template.push_comment();
const is_async = node.metadata.expression.is_async();
const has_await = node.metadata.expression.has_await;
const has_blockers = node.metadata.expression.has_blockers();
const expression = build_expression(context, node.expression, node.metadata.expression);
const key = b.thunk(is_async ? b.call('$.get', b.id('$$key')) : expression);
const key = b.thunk(has_await ? b.call('$.get', b.id('$$key')) : expression);
const body = /** @type {Expression} */ (context.visit(node.fragment));
let statement = add_svelte_meta(
const statement = add_svelte_meta(
b.call('$.key', context.state.node, key, b.arrow([b.id('$$anchor')], body)),
node,
'key'
);
if (is_async) {
statement = b.stmt(
b.call(
'$.async',
context.state.node,
node.metadata.expression.blockers(),
b.array([b.thunk(expression, node.metadata.expression.has_await)]),
b.arrow([context.state.node, b.id('$$key')], b.block([statement]))
if (has_await || has_blockers) {
context.state.init.push(
b.stmt(
b.call(
'$.async',
context.state.node,
node.metadata.expression.blockers(),
has_await ? b.array([b.thunk(expression, true)]) : b.void0,
b.arrow(
has_await ? [context.state.node, b.id('$$key')] : [context.state.node],
b.block([statement])
)
)
)
);
} else {
context.state.init.push(statement);
}
context.state.init.push(statement);
}

@ -11,7 +11,12 @@ import {
import { is_ignored } from '../../../../state.js';
import { is_event_attribute, is_text_attribute } from '../../../../utils/ast.js';
import * as b from '#compiler/builders';
import { create_attribute, ExpressionMetadata, is_custom_element_node } from '../../../nodes.js';
import {
create_attribute,
ExpressionMetadata,
is_custom_element_node,
is_customizable_select_element
} from '../../../nodes.js';
import { clean_nodes, determine_namespace_for_children } from '../../utils.js';
import { build_getter } from '../utils.js';
import {
@ -21,9 +26,12 @@ import {
build_set_class,
build_set_style
} from './shared/element.js';
import { process_children } from './shared/fragment.js';
import { process_children, is_static_element } from './shared/fragment.js';
import { build_render_statement, build_template_chunk, Memoizer } from './shared/utils.js';
import { visit_event_attribute } from './shared/events.js';
import { Template } from '../transform-template/template.js';
import { transform_template } from '../transform-template/index.js';
import { TEMPLATE_FRAGMENT } from '../../../../../constants.js';
/**
* @param {AST.RegularElement} node
@ -248,10 +256,7 @@ export function RegularElement(node, context) {
}
if (name !== 'class' || value) {
context.state.template.set_prop(
attribute.name,
is_boolean_attribute(name) && value === true ? undefined : value === true ? '' : value
);
context.state.template.set_prop(attribute.name, value === true ? '' : value);
}
} else if (name === 'autofocus') {
let { value } = build_attribute_value(attribute.value, context);
@ -321,7 +326,7 @@ export function RegularElement(node, context) {
);
/** @type {typeof state} */
const child_state = { ...state, init: [], update: [], after_update: [] };
const child_state = { ...state, init: [], update: [], after_update: [], snippets: [] };
for (const node of hoisted) {
context.visit(node, child_state);
@ -336,7 +341,9 @@ export function RegularElement(node, context) {
trimmed.every(
(node) =>
node.type === 'Text' ||
(!node.metadata.expression.has_state && !node.metadata.expression.has_await)
(!node.metadata.expression.has_state &&
!node.metadata.expression.has_await &&
!node.metadata.expression.has_blockers())
) &&
trimmed.some((node) => node.type === 'ExpressionTag');
@ -349,13 +356,65 @@ export function RegularElement(node, context) {
b.stmt(b.assignment('=', b.member(context.state.node, 'textContent'), value))
);
}
} else if (is_customizable_select_element(node)) {
// For <option>, <optgroup>, or <select> elements with rich content, we need to branch based on browser support.
// Modern browsers preserve rich HTML in options, older browsers strip it to text only.
// We create a separate template for the rich content and append it to the element.
const element_node = context.state.node;
// Add a hydration marker inside the option element so $.child() has an anchor to find
context.state.template.push_comment();
// Create a separate template for the rich content
const template_name = context.state.scope.root.unique(`${node.name}_content`);
const fragment_id = b.id(context.state.scope.generate('fragment'));
const anchor_id = b.id(context.state.scope.generate('anchor'));
// Create state with a new template for the rich content
/** @type {typeof state} */
const select_state = {
...state,
init: [],
update: [],
after_update: [],
template: new Template()
};
process_children(
trimmed,
(is_text) => b.call('$.first_child', fragment_id, is_text && b.true),
false,
{
...context,
state: select_state
}
);
// Transform the template to $.from_html(...) and hoist it
const template = transform_template(select_state, metadata.namespace, TEMPLATE_FRAGMENT);
context.state.hoisted.push(b.var(template_name, template));
// Build the rich content function body
// The anchor is the child of the element (a hydration marker during hydration)
const body = b.block([
b.var(anchor_id, b.call('$.child', element_node)),
b.var(fragment_id, b.call(template_name)),
...select_state.init,
...(select_state.update.length > 0 ? [build_render_statement(select_state)] : []),
...select_state.after_update,
b.stmt(b.call('$.append', anchor_id, fragment_id))
]);
child_state.init.push(b.stmt(b.call('$.customizable_select', element_node, b.arrow([], body))));
} else {
/** @type {Expression} */
let arg = context.state.node;
// If `hydrate_node` is set inside the element, we need to reset it
// after the element has been hydrated
let needs_reset = trimmed.some((node) => node.type !== 'Text');
// after the element has been hydrated. We need to check if any child
// would actually advance the hydrate_node cursor - static elements don't.
let needs_reset = trimmed.some((node) => node.type !== 'Text' && !is_static_element(node));
// The same applies if it's a `<template>` element, since we need to
// set the value of `hydrate_node` to `node.content`
@ -379,6 +438,7 @@ export function RegularElement(node, context) {
// Wrap children in `{...}` to avoid declaration conflicts
context.state.init.push(
b.block([
...child_state.snippets,
...child_state.init,
...element_state.init,
child_state.update.length > 0 ? build_render_statement(child_state) : b.empty,
@ -395,6 +455,18 @@ export function RegularElement(node, context) {
context.state.after_update.push(...element_state.after_update);
}
if (node.name === 'selectedcontent') {
context.state.init.push(
b.stmt(
b.call(
'$.selectedcontent',
context.state.node,
b.arrow([b.id('$$element')], b.assignment('=', context.state.node, b.id('$$element')))
)
)
);
}
if (lookup.has('dir')) {
// This fixes an issue with Chromium where updates to text content within an element
// does not update the direction when set to auto. If we just re-assign the dir, this fixes it.

@ -85,6 +85,10 @@ export function RenderTag(node, context) {
)
)
);
if (context.state.is_standalone) {
context.state.init.push(b.stmt(b.call('$.next')));
}
} else {
context.state.init.push(statements.length === 1 ? statements[0] : b.block(statements));
}

@ -89,6 +89,6 @@ export function SnippetBlock(node, context) {
context.state.instance_level_snippets.push(declaration);
}
} else {
context.state.init.push(declaration);
context.state.snippets.push(declaration);
}
}

@ -77,7 +77,7 @@ export function SvelteBoundary(node, context) {
/** @type {Statement[]} */
const statements = [];
context.visit(child, { ...context.state, init: statements });
context.visit(child, { ...context.state, snippets: statements });
const snippet = /** @type {VariableDeclaration} */ (statements[0]);

@ -93,10 +93,11 @@ export function SvelteElement(node, context) {
);
}
const is_async = node.metadata.expression.is_async();
const has_await = node.metadata.expression.has_await;
const has_blockers = node.metadata.expression.has_blockers();
const expression = /** @type {Expression} */ (context.visit(node.tag));
const get_tag = b.thunk(is_async ? b.call('$.get', b.id('$$tag')) : expression);
const get_tag = b.thunk(has_await ? b.call('$.get', b.id('$$tag')) : expression);
/** @type {Statement[]} */
const inner = inner_context.state.init;
@ -117,10 +118,10 @@ export function SvelteElement(node, context) {
);
if (dev) {
statements.push(b.stmt(b.call('$.validate_dynamic_element_tag', get_tag)));
if (node.fragment.nodes.length > 0) {
statements.push(b.stmt(b.call('$.validate_void_dynamic_element', get_tag)));
}
statements.push(b.stmt(b.call('$.validate_dynamic_element_tag', get_tag)));
}
const location = dev && locator(node.start);
@ -139,15 +140,18 @@ export function SvelteElement(node, context) {
)
);
if (is_async) {
if (has_await || has_blockers) {
context.state.init.push(
b.stmt(
b.call(
'$.async',
context.state.node,
node.metadata.expression.blockers(),
b.array([b.thunk(expression, node.metadata.expression.has_await)]),
b.arrow([context.state.node, b.id('$$tag')], b.block(statements))
has_await ? b.array([b.thunk(expression, true)]) : b.void0,
b.arrow(
has_await ? [context.state.node, b.id('$$tag')] : [context.state.node],
b.block(statements)
)
)
)
);

@ -209,11 +209,11 @@ export function VariableDeclaration(node, context) {
let call = b.call(
'$.async_derived',
b.thunk(expression, true),
dev && b.literal(declarator.id.name),
location ? b.literal(location) : undefined
);
call = should_save ? save(call) : b.await(call);
if (dev) call = b.call('$.tag', call, b.literal(declarator.id.name));
declarations.push(b.declarator(declarator.id, call));
} else {
@ -244,17 +244,16 @@ export function VariableDeclaration(node, context) {
call = b.call(
'$.async_derived',
b.thunk(expression, true),
dev &&
b.literal(
`[$derived ${declarator.id.type === 'ArrayPattern' ? 'iterable' : 'object'}]`
),
location ? b.literal(location) : undefined
);
call = should_save ? save(call) : b.await(call);
}
if (dev) {
const label = `[$derived ${declarator.id.type === 'ArrayPattern' ? 'iterable' : 'object'}]`;
call = b.call('$.tag', call, b.literal(label));
}
declarations.push(b.declarator(id, call));
}

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

Loading…
Cancel
Save