Merge branch 'main' into fix/a11y-click-events-table-row

pull/17990/head
Razin Shafayet 5 months ago committed by GitHub
commit 4e76efacb4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

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

@ -1,5 +0,0 @@
---
'svelte': patch
---
fix: prevent hydration error on async `{@html ...}`

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

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

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

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

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

2
.gitignore vendored

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

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

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

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

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

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

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

@ -1,12 +1,17 @@
import { reactivity_benchmarks } from '../benchmarks/reactivity/index.js'; import { reactivity_benchmarks } from '../benchmarks/reactivity/index.js';
import { with_cpu_profile } from '../utils.js';
const results = []; const results = [];
const PROFILE_DIR = process.env.BENCH_PROFILE_DIR;
for (let i = 0; i < reactivity_benchmarks.length; i += 1) { for (let i = 0; i < reactivity_benchmarks.length; i += 1) {
const benchmark = reactivity_benchmarks[i]; const benchmark = reactivity_benchmarks[i];
process.stderr.write(`Running ${i + 1}/${reactivity_benchmarks.length} ${benchmark.label} `); process.stderr.write(`Running ${i + 1}/${reactivity_benchmarks.length} ${benchmark.label} `);
results.push({ benchmark: benchmark.label, ...(await benchmark.fn()) }); results.push({
benchmark: benchmark.label,
...(await with_cpu_profile(PROFILE_DIR, benchmark.label, () => benchmark.fn()))
});
process.stderr.write('\x1b[2K\r'); process.stderr.write('\x1b[2K\r');
} }

@ -1,10 +1,13 @@
import * as $ from '../packages/svelte/src/internal/client/index.js'; import * as $ from '../packages/svelte/src/internal/client/index.js';
import { reactivity_benchmarks } from './benchmarks/reactivity/index.js'; import { reactivity_benchmarks } from './benchmarks/reactivity/index.js';
import { ssr_benchmarks } from './benchmarks/ssr/index.js'; import { ssr_benchmarks } from './benchmarks/ssr/index.js';
import { with_cpu_profile } from './utils.js';
// e.g. `pnpm bench kairo` to only run the kairo benchmarks // e.g. `pnpm bench kairo` to only run the kairo benchmarks
const filters = process.argv.slice(2); const filters = process.argv.slice(2);
const PROFILE_DIR = './benchmarking/.profiles';
const suites = [ const suites = [
{ {
benchmarks: reactivity_benchmarks.filter( benchmarks: reactivity_benchmarks.filter(
@ -50,7 +53,7 @@ try {
console.log('='.repeat(TOTAL_WIDTH)); console.log('='.repeat(TOTAL_WIDTH));
for (const benchmark of benchmarks) { for (const benchmark of benchmarks) {
const results = await benchmark.fn(); const results = await with_cpu_profile(PROFILE_DIR, benchmark.label, () => benchmark.fn());
console.log( console.log(
pad_right(benchmark.label, COLUMN_WIDTHS[0]) + pad_right(benchmark.label, COLUMN_WIDTHS[0]) +
pad_left(results.time.toFixed(2), COLUMN_WIDTHS[1]) + pad_left(results.time.toFixed(2), COLUMN_WIDTHS[1]) +
@ -70,6 +73,10 @@ try {
); );
console.log('='.repeat(TOTAL_WIDTH)); console.log('='.repeat(TOTAL_WIDTH));
} }
if (PROFILE_DIR !== null) {
console.log(`\nCPU profiles written to ${PROFILE_DIR}`);
}
} catch (e) { } catch (e) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.error(e); console.error(e);

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

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

@ -40,22 +40,9 @@ If you want to use one of these features, you need to setup up a `script` prepro
To use non-type-only TypeScript features within Svelte components, you need to add a preprocessor that will turn TypeScript into JavaScript. To use non-type-only TypeScript features within Svelte components, you need to add a preprocessor that will turn TypeScript into JavaScript.
```ts ### Using Vite
/// file: svelte.config.js
// @noErrors
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
const config = { If you're using SvelteKit, or Vite _without_ SvelteKit, you can use `vitePreprocess` from `@sveltejs/vite-plugin-svelte` in your config file:
// Note the additional `{ script: true }`
preprocess: vitePreprocess({ script: true })
};
export default config;
```
### Using SvelteKit or Vite
The easiest way to get started is scaffolding a new SvelteKit project by typing `npx sv create`, following the prompts and choosing the TypeScript option.
```ts ```ts
/// file: svelte.config.js /// file: svelte.config.js
@ -63,19 +50,16 @@ The easiest way to get started is scaffolding a new SvelteKit project by typing
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
const config = { const config = {
preprocess: vitePreprocess() // Note the additional `{ script: true }`
preprocess: vitePreprocess({ script: true })
}; };
export default config; export default config;
``` ```
If you don't need or want all the features SvelteKit has to offer, you can scaffold a Svelte-flavoured Vite project instead by typing `npm create vite@latest` and selecting the `svelte-ts` option. ### Using other build tools
In both cases, a `svelte.config.js` with `vitePreprocess` will be added. Vite/SvelteKit will read from this config file.
### Other build tools
If you're using tools like Rollup or Webpack instead, install their respective Svelte plugins. For Rollup that's [rollup-plugin-svelte](https://github.com/sveltejs/rollup-plugin-svelte) and for Webpack that's [svelte-loader](https://github.com/sveltejs/svelte-loader). For both, you need to install `typescript` and `svelte-preprocess` and add the preprocessor to the plugin config (see the respective READMEs for more info). If you're using tools like Rollup (via [rollup-plugin-svelte](https://github.com/sveltejs/rollup-plugin-svelte)) or Webpack (via [svelte-loader](https://github.com/sveltejs/svelte-loader)) instead, install `typescript` and `svelte-preprocess` and add the preprocessor to the plugin config. See the respective plugin READMEs for more info.
> [!NOTE] If you're starting a new project, we recommend using SvelteKit or Vite instead > [!NOTE] If you're starting a new project, we recommend using SvelteKit or Vite instead
@ -85,7 +69,7 @@ When using TypeScript, make sure your `tsconfig.json` is setup correctly.
- Use a [`target`](https://www.typescriptlang.org/tsconfig/#target) of at least `ES2015` so classes are not compiled to functions - Use a [`target`](https://www.typescriptlang.org/tsconfig/#target) of at least `ES2015` so classes are not compiled to functions
- Set [`verbatimModuleSyntax`](https://www.typescriptlang.org/tsconfig/#verbatimModuleSyntax) to `true` so that imports are left as-is - Set [`verbatimModuleSyntax`](https://www.typescriptlang.org/tsconfig/#verbatimModuleSyntax) to `true` so that imports are left as-is
- Set [`isolatedModules`](https://www.typescriptlang.org/tsconfig/#isolatedModules) to `true` so that each file is looked at in isolation. TypeScript has a few features which require cross-file analysis and compilation, which the Svelte compiler and tooling like Vite don't do. - Set [`isolatedModules`](https://www.typescriptlang.org/tsconfig/#isolatedModules) to `true` so that each file is looked at in isolation. TypeScript has a few features which require cross-file analysis and compilation, which the Svelte compiler and tooling like Vite don't do.
## Typing `$props` ## Typing `$props`

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

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

@ -1,5 +1,41 @@
# svelte # svelte
## 5.55.2
### Patch Changes
- fix: invalidate `@const` tags based on visible references in legacy mode ([#18041](https://github.com/sveltejs/svelte/pull/18041))
- fix: handle parens in template expressions more robustly ([#18075](https://github.com/sveltejs/svelte/pull/18075))
- fix: disallow `--` in `idPrefix` ([#18038](https://github.com/sveltejs/svelte/pull/18038))
- fix: correct types for `ontoggle` on `<details>` elements ([#18063](https://github.com/sveltejs/svelte/pull/18063))
- fix: don't override `$destroy/set/on` instance methods in dev mode ([#18034](https://github.com/sveltejs/svelte/pull/18034))
- fix: unskip branches of earlier batches after commit ([#18048](https://github.com/sveltejs/svelte/pull/18048))
- fix: never set derived.v inside fork ([#18037](https://github.com/sveltejs/svelte/pull/18037))
- fix: skip rebase logic in non-async mode ([#18040](https://github.com/sveltejs/svelte/pull/18040))
- fix: don't reset status of uninitialized deriveds ([#18054](https://github.com/sveltejs/svelte/pull/18054))
## 5.55.1
### Patch Changes
- fix: correctly handle bindings on the server ([#18009](https://github.com/sveltejs/svelte/pull/18009))
- fix: prevent hydration error on async `{@html ...}` ([#17999](https://github.com/sveltejs/svelte/pull/17999))
- fix: cleanup `superTypeParameters` in `ClassDeclarations`/`ClassExpression` ([#18015](https://github.com/sveltejs/svelte/pull/18015))
- fix: improve duplicate module import error message ([#18016](https://github.com/sveltejs/svelte/pull/18016))
- fix: reschedule new effects in prior batches ([#18021](https://github.com/sveltejs/svelte/pull/18021))
## 5.55.0 ## 5.55.0
### Minor Changes ### Minor Changes

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

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

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

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

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

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

@ -11,8 +11,6 @@ import { is_reserved } from '../../../utils.js';
import { disallow_children } from '../2-analyze/visitors/shared/special-element.js'; import { disallow_children } from '../2-analyze/visitors/shared/special-element.js';
import * as state from '../../state.js'; import * as state from '../../state.js';
const regex_position_indicator = / \(\d+:\d+\)$/;
/** @param {number} cc */ /** @param {number} cc */
function is_whitespace(cc) { function is_whitespace(cc) {
// fast path for common whitespace // fast path for common whitespace
@ -175,14 +173,6 @@ export class Parser {
return this.stack[this.stack.length - 1]; return this.stack[this.stack.length - 1];
} }
/**
* @param {any} err
* @returns {never}
*/
acorn_error(err) {
e.js_parse_error(err.pos, err.message.replace(regex_position_indicator, ''));
}
/** /**
* @param {string} str * @param {string} str
* @param {boolean} required * @param {boolean} required

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

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

@ -31,14 +31,7 @@ export function read_script(parser, start, attributes) {
parser.template.slice(0, script_start).replace(regex_not_newline_characters, ' ') + data; parser.template.slice(0, script_start).replace(regex_not_newline_characters, ' ') + data;
parser.read(regex_starts_with_closing_script_tag); parser.read(regex_starts_with_closing_script_tag);
/** @type {Program} */ const ast = acorn.parse(source, parser.root.comments, parser.ts, true);
let ast;
try {
ast = acorn.parse(source, parser.root.comments, parser.ts, true);
} catch (err) {
parser.acorn_error(err);
}
ast.start = script_start; ast.start = script_start;

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

@ -392,12 +392,7 @@ function open(parser) {
let function_expression = matched let function_expression = matched
? /** @type {ArrowFunctionExpression} */ ( ? /** @type {ArrowFunctionExpression} */ (
parse_expression_at( parse_expression_at(parser, prelude + `${params} => {}`, params_start)
prelude + `${params} => {}`,
parser.root.comments,
parser.ts,
params_start
)
) )
: { params: [] }; : { params: [] };

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

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

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

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

@ -145,6 +145,12 @@ export class Batch {
*/ */
#roots = []; #roots = [];
/**
* Effects created while this batch was active.
* @type {Effect[]}
*/
#new_effects = [];
/** /**
* Deferred effects (which run after async work has completed) that are DIRTY * Deferred effects (which run after async work has completed) that are DIRTY
* @type {Set<Effect>} * @type {Set<Effect>}
@ -166,6 +172,12 @@ export class Batch {
*/ */
#skipped_branches = new Map(); #skipped_branches = new Map();
/**
* Inverse of #skipped_branches which we need to tell prior batches to unskip them when committing
* @type {Set<Effect>}
*/
#unskipped_branches = new Set();
is_fork = false; is_fork = false;
#decrement_queued = false; #decrement_queued = false;
@ -209,28 +221,31 @@ export class Batch {
if (!this.#skipped_branches.has(effect)) { if (!this.#skipped_branches.has(effect)) {
this.#skipped_branches.set(effect, { d: [], m: [] }); this.#skipped_branches.set(effect, { d: [], m: [] });
} }
this.#unskipped_branches.delete(effect);
} }
/** /**
* Remove an effect from the #skipped_branches map and reschedule * Remove an effect from the #skipped_branches map and reschedule
* any tracked dirty/maybe_dirty child effects * any tracked dirty/maybe_dirty child effects
* @param {Effect} effect * @param {Effect} effect
* @param {(e: Effect) => void} callback
*/ */
unskip_effect(effect) { unskip_effect(effect, callback = (e) => this.schedule(e)) {
var tracked = this.#skipped_branches.get(effect); var tracked = this.#skipped_branches.get(effect);
if (tracked) { if (tracked) {
this.#skipped_branches.delete(effect); this.#skipped_branches.delete(effect);
for (var e of tracked.d) { for (var e of tracked.d) {
set_signal_status(e, DIRTY); set_signal_status(e, DIRTY);
this.schedule(e); callback(e);
} }
for (e of tracked.m) { for (e of tracked.m) {
set_signal_status(e, MAYBE_DIRTY); set_signal_status(e, MAYBE_DIRTY);
this.schedule(e); callback(e);
} }
} }
this.#unskipped_branches.add(effect);
} }
#process() { #process() {
@ -343,7 +358,9 @@ export class Batch {
next_batch.#process(); next_batch.#process();
} }
if (!batches.has(this)) { // In sync mode flushSync can cause #commit to wrongfully think that there needs to be a rebase, so we only do it in async mode
// TODO fix the underlying cause, otherwise this will likely regress when non-async mode is removed
if (async_mode_flag && !batches.has(this)) {
this.#commit(); this.#commit();
} }
} }
@ -413,18 +430,22 @@ export class Batch {
* Associate a change to a given source with the current * Associate a change to a given source with the current
* batch, noting its previous and current values * batch, noting its previous and current values
* @param {Value} source * @param {Value} source
* @param {any} old_value * @param {any} value
* @param {boolean} [is_derived] * @param {boolean} [is_derived]
*/ */
capture(source, old_value, is_derived = false) { capture(source, value, is_derived = false) {
if (old_value !== UNINITIALIZED && !this.previous.has(source)) { if (source.v !== UNINITIALIZED && !this.previous.has(source)) {
this.previous.set(source, old_value); this.previous.set(source, source.v);
} }
// Don't save errors in `batch_values`, or they won't be thrown in `runtime.js#get` // Don't save errors in `batch_values`, or they won't be thrown in `runtime.js#get`
if ((source.f & ERROR_VALUE) === 0) { if ((source.f & ERROR_VALUE) === 0) {
this.current.set(source, [source.v, is_derived]); this.current.set(source, [value, is_derived]);
batch_values?.set(source, source.v); batch_values?.set(source, value);
}
if (!this.is_fork) {
source.v = value;
} }
} }
@ -472,6 +493,13 @@ export class Batch {
batches.delete(this); batches.delete(this);
} }
/**
* @param {Effect} effect
*/
register_created_effect(effect) {
this.#new_effects.push(effect);
}
#commit() { #commit() {
// If there are other pending batches, they now need to be 'rebased' — // If there are other pending batches, they now need to be 'rebased' —
// in other words, we re-run block/async effects with the newly // in other words, we re-run block/async effects with the newly
@ -513,6 +541,19 @@ export class Batch {
invariant(batch.#roots.length === 0, 'Batch has scheduled roots'); invariant(batch.#roots.length === 0, 'Batch has scheduled roots');
} }
// A batch was unskipped in a later batch -> tell prior batches to unskip it, too
if (is_earlier) {
for (const unskipped of this.#unskipped_branches) {
batch.unskip_effect(unskipped, (e) => {
if ((e.f & (BLOCK_EFFECT | ASYNC)) !== 0) {
batch.schedule(e);
} else {
batch.#defer_effects([e]);
}
});
}
}
batch.activate(); batch.activate();
/** @type {Set<Value>} */ /** @type {Set<Value>} */
@ -525,6 +566,25 @@ export class Batch {
mark_effects(source, others, marked, checked); mark_effects(source, others, marked, checked);
} }
checked = new Map();
var current_unequal = [...batch.current.keys()].filter((c) =>
this.current.has(c) ? /** @type {[any, boolean]} */ (this.current.get(c))[0] !== c : true
);
for (const effect of this.#new_effects) {
if (
(effect.f & (DESTROYED | INERT | EAGER_EFFECT)) === 0 &&
depends_on(effect, current_unequal, checked)
) {
if ((effect.f & (ASYNC | BLOCK_EFFECT)) !== 0) {
set_signal_status(effect, DIRTY);
batch.schedule(effect);
} else {
batch.#dirty_effects.add(effect);
}
}
}
// Only apply and traverse when we know we triggered async work with marking the effects // Only apply and traverse when we know we triggered async work with marking the effects
if (batch.#roots.length > 0) { if (batch.#roots.length > 0) {
batch.apply(); batch.apply();
@ -755,6 +815,7 @@ export class Batch {
} }
} }
// TODO Svelte@6 think about removing the callback argument.
/** /**
* Synchronously flush any pending updates. * Synchronously flush any pending updates.
* Returns void if no callback is provided, otherwise returns the result of calling the callback. * Returns void if no callback is provided, otherwise returns the result of calling the callback.
@ -1130,11 +1191,6 @@ export function fork(fn) {
flushSync(fn); flushSync(fn);
// revert state changes
for (var [source, value] of batch.previous) {
source.v = value;
}
return { return {
commit: async () => { commit: async () => {
if (committed) { if (committed) {

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

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

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

@ -399,7 +399,14 @@ function remove_reaction(signal, dependency) {
derived.f &= ~WAS_MARKED; derived.f &= ~WAS_MARKED;
} }
update_derived_status(derived); // In a fork it's possible that a derived is executed and gets reactions, then commits, but is
// never re-executed. This is possible when the derived is only executed once in the context
// of a new branch which happens before fork.commit() runs. In this case, the derived still has
// UNINITIALIZED as its value, and then when it's loosing its reactions we need to ensure it stays
// DIRTY so it is reexecuted once someone wants its value again.
if (derived.v !== UNINITIALIZED) {
update_derived_status(derived);
}
// freeze any effects inside this derived // freeze any effects inside this derived
freeze_derived_effects(derived); freeze_derived_effects(derived);

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

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

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

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

@ -0,0 +1,61 @@
{
"css": null,
"js": [],
"start": 0,
"end": 11,
"type": "Root",
"fragment": {
"type": "Fragment",
"nodes": [
{
"type": "ExpressionTag",
"start": 0,
"end": 11,
"expression": {
"type": "Literal",
"start": 7,
"end": 9,
"loc": {
"start": {
"line": 1,
"column": 7
},
"end": {
"line": 1,
"column": 9
}
},
"value": 42,
"raw": "42",
"leadingComments": [
{
"type": "Block",
"value": "",
"start": 2,
"end": 6
}
]
}
}
]
},
"options": null,
"comments": [
{
"type": "Block",
"value": "",
"start": 2,
"end": 6,
"loc": {
"start": {
"line": 1,
"column": 2
},
"end": {
"line": 1,
"column": 6
}
}
}
]
}

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

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

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

@ -0,0 +1,20 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target }) {
await tick();
const [btn] = target.querySelectorAll('button');
btn.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>inc</button>
<my-inner value="2"></my-inner>
2
`
);
}
});

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

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

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

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

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

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

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

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

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

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

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

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

@ -2,7 +2,6 @@ import { tick } from 'svelte';
import { test } from '../../test'; import { test } from '../../test';
export default test({ export default test({
skip: true, // TODO works on https://github.com/sveltejs/svelte/pull/17971
async test({ assert, target, logs }) { async test({ assert, target, logs }) {
const [x, y, resolve] = target.querySelectorAll('button'); const [x, y, resolve] = target.querySelectorAll('button');
@ -17,12 +16,20 @@ export default test({
<button>x</button> <button>x</button>
<button>y++</button> <button>y++</button>
<button>resolve</button> <button>resolve</button>
` // if this shows world world - that would also be ok world
` // if this does not show world - that would also be ok
); );
resolve.click(); resolve.click();
await tick(); await tick();
assert.deepEqual(logs, ['universe', 'universe', '$effect: universe', '$effect: universe']); assert.deepEqual(logs, [
'universe',
'world',
'$effect: world',
'$effect: universe',
'$effect: universe'
]);
// assert.deepEqual(logs, ['universe', 'universe', '$effect: universe', '$effect: universe']); // this would also be ok
assert.htmlEqual( assert.htmlEqual(
target.innerHTML, target.innerHTML,
` `

@ -2,7 +2,6 @@ import { tick } from 'svelte';
import { test } from '../../test'; import { test } from '../../test';
export default test({ export default test({
skip: true, // TODO works on https://github.com/sveltejs/svelte/pull/17971
async test({ assert, target }) { async test({ assert, target }) {
const [x, y, resolve] = target.querySelectorAll('button'); const [x, y, resolve] = target.querySelectorAll('button');
@ -18,7 +17,13 @@ export default test({
<button>y++</button> <button>y++</button>
<button>resolve</button> <button>resolve</button>
<hr> <hr>
` // if this shows world world "world" world world world "world" - then this would also be ok world
"world"
world
world
world
"world"
` // if this does not show world "world" world world world "world" - then this would also be ok
); );
resolve.click(); resolve.click();

@ -2,7 +2,6 @@ import { tick } from 'svelte';
import { test } from '../../test'; import { test } from '../../test';
export default test({ export default test({
skip: true, // TODO works on https://github.com/sveltejs/svelte/pull/17971
async test({ assert, target }) { async test({ assert, target }) {
const [x, y, resolve] = target.querySelectorAll('button'); const [x, y, resolve] = target.querySelectorAll('button');
@ -30,9 +29,17 @@ export default test({
<button>y++</button> <button>y++</button>
<button>resolve</button> <button>resolve</button>
<hr> <hr>
` // if this shows world world "world" world world world "world" - then this would also be ok world
"world"
world
world
world
"world"
` // if this does not show world "world" world world world "world" - then this would also be ok
); );
resolve.click();
await tick();
resolve.click(); resolve.click();
await tick(); await tick();
assert.htmlEqual( assert.htmlEqual(

@ -31,4 +31,3 @@
{#if y > 0} {#if y > 0}
<Child x={await delay2(x)} /> <Child x={await delay2(x)} />
{/if} {/if}

@ -2,7 +2,6 @@ import { tick } from 'svelte';
import { test } from '../../test'; import { test } from '../../test';
export default test({ export default test({
skip: true, // TODO works on https://github.com/sveltejs/svelte/pull/17971
async test({ assert, target }) { async test({ assert, target }) {
const [x, y, shift, pop, commit] = target.querySelectorAll('button'); const [x, y, shift, pop, commit] = target.querySelectorAll('button');
@ -43,6 +42,8 @@ export default test({
await tick(); await tick();
shift.click(); shift.click();
await tick(); await tick();
shift.click(); // would be ok to not need this one
await tick();
assert.htmlEqual( assert.htmlEqual(
target.innerHTML, target.innerHTML,
` `

@ -2,7 +2,6 @@ import { tick } from 'svelte';
import { test } from '../../test'; import { test } from '../../test';
export default test({ export default test({
skip: true, // TODO works on https://github.com/sveltejs/svelte/pull/17971
async test({ assert, target }) { async test({ assert, target }) {
const [x, y, resolve, commit] = target.querySelectorAll('button'); const [x, y, resolve, commit] = target.querySelectorAll('button');

@ -2,7 +2,6 @@ import { tick } from 'svelte';
import { test } from '../../test'; import { test } from '../../test';
export default test({ export default test({
skip: true, // this fails on main, too; skip for now
async test({ assert, target, logs }) { async test({ assert, target, logs }) {
const [x, y, resolve] = target.querySelectorAll('button'); const [x, y, resolve] = target.querySelectorAll('button');

@ -3,5 +3,7 @@ import { test } from '../../test';
export default test({ export default test({
skip_no_async: true, skip_no_async: true,
mode: ['hydrate'], mode: ['hydrate'],
async test() {} async test({ assert, warnings }) {
assert.deepEqual(warnings, []); // TODO not quite sure why this isn't populated yet
}
}); });

@ -27,6 +27,11 @@
abstract x(): void; abstract x(): void;
y() {} y() {}
} }
class Subclass extends Foo<string> {
constructor(value: string) {
super(value);
}
}
declare const declared_const: number; declare const declared_const: number;
declare function declared_fn(): void; declare function declared_fn(): void;

@ -1,7 +1,7 @@
[ [
{ {
"code": "declaration_duplicate_module_import", "code": "declaration_duplicate_module_import",
"message": "Cannot declare a variable with the same name as an import inside `<script module>`", "message": "Cannot declare a variable with the same name as an import from `<script module>`",
"start": { "start": {
"line": 12, "line": 12,
"column": 5 "column": 5

@ -21,7 +21,7 @@
"polka": "^1.0.0-next.25", "polka": "^1.0.0-next.25",
"svelte": "workspace:*", "svelte": "workspace:*",
"tinyglobby": "^0.2.12", "tinyglobby": "^0.2.12",
"vite": "^7.1.11", "vite": "^7.3.2",
"vite-plugin-devtools-json": "^1.0.0", "vite-plugin-devtools-json": "^1.0.0",
"vite-plugin-inspect": "^11.3.3" "vite-plugin-inspect": "^11.3.3"
} }

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save