diff --git a/.agents/skills/performance-investigation/SKILL.md b/.agents/skills/performance-investigation/SKILL.md new file mode 100644 index 0000000000..cbc5d81882 --- /dev/null +++ b/.agents/skills/performance-investigation/SKILL.md @@ -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/.json` +- CPU profiles (per benchmark, per branch): + - `benchmarking/compare/.profiles/main/*.cpuprofile` + - `benchmarking/compare/.profiles/main/*.md` + - `benchmarking/compare/.profiles//*.cpuprofile` + - `benchmarking/compare/.profiles//*.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/.md` + - `benchmarking/compare/.profiles//.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`. diff --git a/.changeset/const-tags.md b/.changeset/const-tags.md new file mode 100644 index 0000000000..82765c5038 --- /dev/null +++ b/.changeset/const-tags.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: invalidate `@const` tags based on visible references in legacy mode diff --git a/.changeset/evil-chicken-attend.md b/.changeset/evil-chicken-attend.md new file mode 100644 index 0000000000..f89e7e17f3 --- /dev/null +++ b/.changeset/evil-chicken-attend.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: disallow `--` in `idPrefix` diff --git a/.changeset/great-toes-behave.md b/.changeset/great-toes-behave.md new file mode 100644 index 0000000000..5f2845e364 --- /dev/null +++ b/.changeset/great-toes-behave.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: correct types for `ontoggle` on `
` elements diff --git a/.changeset/orange-geckos-rest.md b/.changeset/orange-geckos-rest.md new file mode 100644 index 0000000000..5717c249b6 --- /dev/null +++ b/.changeset/orange-geckos-rest.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: don't override `$destroy/set/on` instance methods in dev mode diff --git a/.changeset/petite-signs-flash.md b/.changeset/petite-signs-flash.md new file mode 100644 index 0000000000..38a1c7b47c --- /dev/null +++ b/.changeset/petite-signs-flash.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: unskip branches of earlier batches after commit diff --git a/.changeset/poor-tips-send.md b/.changeset/poor-tips-send.md new file mode 100644 index 0000000000..beb0329705 --- /dev/null +++ b/.changeset/poor-tips-send.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: never set derived.v inside fork diff --git a/.changeset/ripe-mails-wave.md b/.changeset/ripe-mails-wave.md new file mode 100644 index 0000000000..6ccc1c724d --- /dev/null +++ b/.changeset/ripe-mails-wave.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: skip rebase logic in non-async mode diff --git a/.changeset/silent-rings-yell.md b/.changeset/silent-rings-yell.md new file mode 100644 index 0000000000..0b417103f7 --- /dev/null +++ b/.changeset/silent-rings-yell.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: don't reset status of uninitialized deriveds diff --git a/.editorconfig b/.editorconfig index 2f52d9993f..900cdf7cdc 100644 --- a/.editorconfig +++ b/.editorconfig @@ -4,7 +4,6 @@ root = true end_of_line = lf insert_final_newline = true indent_style = tab -indent_size = 2 charset = utf-8 trim_trailing_whitespace = true diff --git a/.github/workflows/autofix.yml b/.github/workflows/autofix.yml index 706a62e3d6..b6c26e0792 100644 --- a/.github/workflows/autofix.yml +++ b/.github/workflows/autofix.yml @@ -50,7 +50,7 @@ jobs: if: github.event_name == 'workflow_dispatch' || steps.pr.outcome == 'success' with: ref: ${{ github.event_name == 'workflow_dispatch' && github.ref || steps.pr.outputs.ref }} - - uses: pnpm/action-setup@v4.3.0 + - uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4.3.0 - uses: actions/setup-node@v6 with: node-version: 24 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 23d814d527..df9f755874 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,7 +33,7 @@ jobs: steps: - uses: actions/checkout@v6 - - uses: pnpm/action-setup@v4 + - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4 - uses: actions/setup-node@v6 with: node-version: ${{ matrix.node-version }} @@ -49,7 +49,7 @@ jobs: timeout-minutes: 10 steps: - uses: actions/checkout@v6 - - uses: pnpm/action-setup@v4 + - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4 - uses: actions/setup-node@v6 with: node-version: 22 @@ -66,7 +66,7 @@ jobs: timeout-minutes: 5 steps: - uses: actions/checkout@v6 - - uses: pnpm/action-setup@v4 + - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4 - uses: actions/setup-node@v6 with: node-version: 24 @@ -83,7 +83,7 @@ jobs: timeout-minutes: 5 steps: - uses: actions/checkout@v6 - - uses: pnpm/action-setup@v4 + - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4 - uses: actions/setup-node@v6 with: node-version: 24 @@ -104,10 +104,10 @@ jobs: timeout-minutes: 15 steps: - uses: actions/checkout@v6 - - uses: pnpm/action-setup@v4 + - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4 - uses: actions/setup-node@v6 with: - node-version: 18 + node-version: 24 cache: pnpm - run: pnpm install --frozen-lockfile - run: pnpm bench diff --git a/.github/workflows/pkg.pr.new.yml b/.github/workflows/pkg.pr.new.yml index cd3df32064..0fcda5a778 100644 --- a/.github/workflows/pkg.pr.new.yml +++ b/.github/workflows/pkg.pr.new.yml @@ -35,7 +35,7 @@ jobs: # For push, fall back to the push SHA. ref: ${{ github.event.pull_request.head.sha || inputs.sha || github.sha }} - - uses: pnpm/action-setup@v4 + - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4 - uses: actions/setup-node@v6 with: node-version: 22.x diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index aa32e3c5cd..359fcb7eea 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -27,7 +27,7 @@ jobs: with: # This makes Actions fetch all Git history so that Changesets can generate changelogs with the correct commits fetch-depth: 0 - - uses: pnpm/action-setup@v4 + - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4 - name: Setup Node.js uses: actions/setup-node@v6 with: @@ -42,7 +42,7 @@ jobs: - name: Create Release Pull Request or Publish to npm id: changesets - uses: changesets/action@v1 + uses: changesets/action@6a0a831ff30acef54f2c6aa1cbbc1096b066edaf # v1 with: version: pnpm changeset:version publish: pnpm changeset:publish diff --git a/.gitignore b/.gitignore index d503437664..d3c1819bd5 100644 --- a/.gitignore +++ b/.gitignore @@ -23,4 +23,6 @@ coverage tmp +benchmarking/.profiles benchmarking/compare/.results +benchmarking/compare/.profiles diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000000..c6cd3ea310 --- /dev/null +++ b/AGENTS.md @@ -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. diff --git a/benchmarking/benchmarks/reactivity/index.js b/benchmarking/benchmarks/reactivity/index.js index 2b75b3dfc6..3fe9639376 100644 --- a/benchmarking/benchmarks/reactivity/index.js +++ b/benchmarking/benchmarks/reactivity/index.js @@ -1,5 +1,6 @@ import fs from 'node:fs'; import path from 'node:path'; +import 'svelte/internal/flags/async'; import { sbench_create_0to1, sbench_create_1000to1, diff --git a/benchmarking/benchmarks/reactivity/tests/clean_effects.bench.js b/benchmarking/benchmarks/reactivity/tests/clean_effects.bench.js new file mode 100644 index 0000000000..a617554f1b --- /dev/null +++ b/benchmarking/benchmarks/reactivity/tests/clean_effects.bench.js @@ -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); + } + } + }; +}; diff --git a/benchmarking/compare/generate-report.js b/benchmarking/compare/generate-report.js new file mode 100644 index 0000000000..a61f58909b --- /dev/null +++ b/benchmarking/compare/generate-report.js @@ -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); +} diff --git a/benchmarking/compare/index.js b/benchmarking/compare/index.js index 8f38686a29..9064ee7da9 100644 --- a/benchmarking/compare/index.js +++ b/benchmarking/compare/index.js @@ -2,6 +2,8 @@ import fs from 'node:fs'; import path from 'node:path'; import { execSync, fork } from 'node:child_process'; import { fileURLToPath } from 'node:url'; +import { safe } from '../utils.js'; +import { generate_report } from './generate-report.js'; // if (execSync('git status --porcelain').toString().trim()) { // console.error('Working directory is not clean'); @@ -12,39 +14,61 @@ const filename = fileURLToPath(import.meta.url); const runner = path.resolve(filename, '../runner.js'); const outdir = path.resolve(filename, '../.results'); -if (fs.existsSync(outdir)) fs.rmSync(outdir, { recursive: true }); -fs.mkdirSync(outdir); +fs.mkdirSync(outdir, { recursive: true }); -const branches = []; +const requested_branches = []; + +let PROFILE_DIR = path.resolve(filename, '../.profiles'); +fs.mkdirSync(PROFILE_DIR, { recursive: true }); for (const arg of process.argv.slice(2)) { if (arg.startsWith('--')) continue; if (arg === filename) continue; - branches.push(arg); + requested_branches.push(arg); } -if (branches.length === 0) { - branches.push( +if (requested_branches.length === 0) { + requested_branches.push( execSync('git symbolic-ref --short -q HEAD || git rev-parse --short HEAD').toString().trim() ); } -if (branches.length === 1) { - branches.push('main'); +const original_ref = execSync('git symbolic-ref --short -q HEAD || git rev-parse --short HEAD') + .toString() + .trim(); + +if ( + requested_branches.length === 1 && + !requested_branches.includes('main') && + !fs.existsSync(`${outdir}/main.json`) +) { + requested_branches.push('main'); } process.on('exit', () => { - execSync(`git checkout ${branches[0]}`); + execSync(`git checkout ${original_ref}`); }); -for (const branch of branches) { +for (const branch of requested_branches) { console.group(`Benchmarking ${branch}`); + const branch_profile_dir = `${PROFILE_DIR}/${safe(branch)}`; + if (fs.existsSync(branch_profile_dir)) + fs.rmSync(branch_profile_dir, { recursive: true, force: true }); + + const branch_result_file = `${outdir}/${branch}.json`; + if (fs.existsSync(branch_result_file)) fs.rmSync(branch_result_file, { force: true }); + execSync(`git checkout ${branch}`); await new Promise((fulfil, reject) => { - const child = fork(runner); + const child = fork(runner, [], { + env: { + ...process.env, + BENCH_PROFILE_DIR: branch_profile_dir + } + }); child.on('message', (results) => { fs.writeFileSync(`${outdir}/${branch}.json`, JSON.stringify(results, null, ' ')); @@ -57,47 +81,8 @@ for (const branch of branches) { console.groupEnd(); } -const results = branches.map((branch) => { - return JSON.parse(fs.readFileSync(`${outdir}/${branch}.json`, 'utf-8')); -}); - -for (let i = 0; i < results[0].length; i += 1) { - console.group(`${results[0][i].benchmark}`); - - for (const metric of ['time', 'gc_time']) { - const times = results.map((result) => +result[i][metric]); - let min = Infinity; - let max = -Infinity; - let min_index = -1; - - for (let b = 0; b < times.length; b += 1) { - const time = times[b]; - - if (time < min) { - min = time; - min_index = b; - } - - if (time > max) { - max = time; - } - } - - if (min !== 0) { - console.group(`${metric}: fastest is ${char(min_index)} (${branches[min_index]})`); - times.forEach((time, b) => { - const SIZE = 20; - const n = Math.round(SIZE * (time / max)); - - console.log(`${char(b)}: ${'◼'.repeat(n)}${' '.repeat(SIZE - n)} ${time.toFixed(2)}ms`); - }); - console.groupEnd(); - } - } - - console.groupEnd(); +if (PROFILE_DIR !== null) { + console.log(`\nCPU profiles written to ${PROFILE_DIR}`); } -function char(i) { - return String.fromCharCode(97 + i); -} +generate_report(outdir); diff --git a/benchmarking/compare/profile-diff.mjs b/benchmarking/compare/profile-diff.mjs new file mode 100644 index 0000000000..c6a9061ab2 --- /dev/null +++ b/benchmarking/compare/profile-diff.mjs @@ -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 ' + ); + 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}` + ); +} diff --git a/benchmarking/compare/runner.js b/benchmarking/compare/runner.js index 11e40ed983..31a8e6b44b 100644 --- a/benchmarking/compare/runner.js +++ b/benchmarking/compare/runner.js @@ -1,12 +1,17 @@ import { reactivity_benchmarks } from '../benchmarks/reactivity/index.js'; +import { with_cpu_profile } from '../utils.js'; const results = []; +const PROFILE_DIR = process.env.BENCH_PROFILE_DIR; for (let i = 0; i < reactivity_benchmarks.length; i += 1) { const benchmark = reactivity_benchmarks[i]; process.stderr.write(`Running ${i + 1}/${reactivity_benchmarks.length} ${benchmark.label} `); - results.push({ benchmark: benchmark.label, ...(await benchmark.fn()) }); + results.push({ + benchmark: benchmark.label, + ...(await with_cpu_profile(PROFILE_DIR, benchmark.label, () => benchmark.fn())) + }); process.stderr.write('\x1b[2K\r'); } diff --git a/benchmarking/run.js b/benchmarking/run.js index 2b09f7c592..80e40a5ff1 100644 --- a/benchmarking/run.js +++ b/benchmarking/run.js @@ -1,10 +1,13 @@ import * as $ from '../packages/svelte/src/internal/client/index.js'; import { reactivity_benchmarks } from './benchmarks/reactivity/index.js'; import { ssr_benchmarks } from './benchmarks/ssr/index.js'; +import { with_cpu_profile } from './utils.js'; // e.g. `pnpm bench kairo` to only run the kairo benchmarks const filters = process.argv.slice(2); +const PROFILE_DIR = './benchmarking/.profiles'; + const suites = [ { benchmarks: reactivity_benchmarks.filter( @@ -50,7 +53,7 @@ try { console.log('='.repeat(TOTAL_WIDTH)); for (const benchmark of benchmarks) { - const results = await benchmark.fn(); + const results = await with_cpu_profile(PROFILE_DIR, benchmark.label, () => benchmark.fn()); console.log( pad_right(benchmark.label, COLUMN_WIDTHS[0]) + pad_left(results.time.toFixed(2), COLUMN_WIDTHS[1]) + @@ -70,6 +73,10 @@ try { ); console.log('='.repeat(TOTAL_WIDTH)); } + + if (PROFILE_DIR !== null) { + console.log(`\nCPU profiles written to ${PROFILE_DIR}`); + } } catch (e) { // eslint-disable-next-line no-console console.error(e); diff --git a/benchmarking/utils.js b/benchmarking/utils.js index 5581135e00..2f4be3c567 100644 --- a/benchmarking/utils.js +++ b/benchmarking/utils.js @@ -1,4 +1,7 @@ import { performance, PerformanceObserver } from 'node:perf_hooks'; +import fs from 'node:fs'; +import path from 'node:path'; +import inspector from 'node:inspector/promises'; import v8 from 'v8-natives'; // Credit to https://github.com/milomg/js-reactivity-benchmark for the logic for timing + GC tracking. @@ -41,3 +44,290 @@ export async function fastest_test(times, fn) { return results.reduce((a, b) => (a.time < b.time ? a : b)); } + +export function safe(name) { + return name.replace(/[^a-z0-9._-]+/gi, '_'); +} + +/** + * @param {unknown} value + */ +function format_markdown_value(value) { + if (value === null || value === undefined) return ''; + if (Array.isArray(value)) return value.map((item) => String(item)).join(', '); + if (typeof value === 'object') return JSON.stringify(value); + return String(value); +} + +/** + * @param {string} text + */ +function escape_markdown_cell(text) { + return text.replace(/\\/g, '\\\\').replace(/\|/g, '\\|').replace(/\r?\n/g, ' '); +} + +/** + * @param {string} value + */ +function normalize_profile_url(value) { + if (!value) return ''; + + if (value.startsWith('file://')) { + try { + const pathname = decodeURIComponent(new URL(value).pathname); + const relative = path.relative(process.cwd(), pathname); + if (relative && !relative.startsWith('..') && !path.isAbsolute(relative)) return relative; + return pathname; + } catch { + return value; + } + } + + if (path.isAbsolute(value)) { + const relative = path.relative(process.cwd(), value); + if (relative && !relative.startsWith('..') && !path.isAbsolute(relative)) return relative; + } + + return value; +} + +/** + * @param {string} function_name + */ +function is_special_runtime_node(function_name) { + return function_name === '(idle)' || function_name === '(garbage collector)'; +} + +/** + * @param {string} normalized_url + */ +function is_svelte_source_url(normalized_url) { + return normalized_url.startsWith('packages/svelte/'); +} + +/** + * @param {Record} 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} */ + const included_node_ids = new Set(); + + if (nodes.length > 0) { + /** @type {Map>} */ + const nodes_by_id = new Map(); + + /** @type {Map} */ + 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} */ (node.callFrame) + : /** @type {Record} */ ({}); + 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} */ + 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} */ + const inclusive_sample_count = new Map(); + /** @type {Set} */ + 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} */ (node.callFrame) + : /** @type {Record} */ ({}); + 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} */ (node.callFrame) + : /** @type {Record} */ ({}); + + 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} fn + * @returns {Promise} + */ +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} */ (profile)) + ); + session.disconnect(); + } +} diff --git a/documentation/docs/02-runes/03-$derived.md b/documentation/docs/02-runes/03-$derived.md index 35cb6c1912..f85ba90baa 100644 --- a/documentation/docs/02-runes/03-$derived.md +++ b/documentation/docs/02-runes/03-$derived.md @@ -51,6 +51,17 @@ In essence, `$derived(expression)` is equivalent to `$derived.by(() => expressio Anything read synchronously inside the `$derived` expression (or `$derived.by` function body) is considered a _dependency_ of the derived state. When the state changes, the derived will be marked as _dirty_ and recalculated when it is next read. +In addition, if an expression contains an [`await`](await-expressions), Svelte transforms it such that any state _after_ the `await` is also tracked — in other words, in a case like this... + +```js +let a = Promise.resolve(1); +let b = 2; +// ---cut--- +let total = $derived(await a + b); +``` + +...both `a` and `b` are tracked, even though `b` is only read once `a` has resolved, after the initial execution. (This does not apply to `await` in functions that are called by the expression, only the expression itself.) + To exempt a piece of state from being treated as a dependency, use [`untrack`](svelte#untrack). ## Overriding derived values diff --git a/documentation/docs/02-runes/04-$effect.md b/documentation/docs/02-runes/04-$effect.md index d41c5b8e6a..a13fc7bc46 100644 --- a/documentation/docs/02-runes/04-$effect.md +++ b/documentation/docs/02-runes/04-$effect.md @@ -41,9 +41,11 @@ You can use `$effect` anywhere, not just at the top level of a component, as lon > [!NOTE] Svelte uses effects internally to represent logic and expressions in your template — this is how `

hello {name}!

` updates when `name` changes. -An effect can return a _teardown function_ which will run immediately before the effect re-runs ([demo](/playground/untitled#H4sIAAAAAAAAE42SQVODMBCF_8pOxkPRKq3HCsx49K4n64xpskjGkDDJ0tph-O8uINo6HjxB3u7HvrehE07WKDbiyZEhi1osRWksRrF57gQdm6E2CKx_dd43zU3co6VB28mIf-nKO0JH_BmRRRVMQ8XWbXkAgfKtI8jhIpIkXKySu7lSG2tNRGZ1_GlYr1ZTD3ddYFmiosUigbyAbpC2lKbwWJkIB8ZhhxBQBWRSw6FCh3sM8GrYTthL-wqqku4N44TyqEgwF3lmRHr4Op0PGXoH31c5rO8mqV-eOZ49bikgtcHBL55tmhIkEMqg_cFB2TpFxjtg703we6NRL8HQFCS07oSUCZi6Rm04lz1yytIHBKoQpo1w6Gsm4gmyS8b8Y5PydeMdX8gwS2Ok4I-ov5NZtvQde95GMsccn_1wzNKfu3RZtS66cSl9lvL7qO1aIk7knbJGvefdtIOzi73M4bYvovUHDFk6AcX_0HRESxnpBOW_jfCDxIZCi_1L_wm4xGQ60wIAAA==)). +An effect can return a _teardown function_ which will run immediately before the effect re-runs: + ```svelte + + @@ -236,6 +254,7 @@ When using [`await`](await-expressions) in components, the `$effect.pending()` r

pending promises: {$effect.pending()}

{/if} ``` + ## `$effect.root` @@ -285,9 +304,11 @@ In general, `$effect` is best considered something of an escape hatch — useful If you're using an effect because you want to be able to reassign the derived value (to build an optimistic UI, for example) note that [deriveds can be directly overridden]($derived#Overriding-derived-values) as of Svelte 5.25. -You might be tempted to do something convoluted with effects to link one value to another. The following example shows two inputs for "money spent" and "money left" that are connected to each other. If you update one, the other should update accordingly. Don't use effects for this ([demo](/playground/untitled#H4sIAAAAAAAAE5WRTWrDMBCFryKGLBJoY3fRjWIHeoiu6i6UZBwEY0VE49TB-O6VxrFTSih0qe_Ne_OjHpxpEDS8O7ZMeIAnqC1hAP3RA1990hKI_Fb55v06XJA4sZ0J-IjvT47RcYyBIuzP1vO2chVHHFjxiQ2pUr3k-SZRQlbBx_LIFoEN4zJfzQph_UMQr4hRXmBd456Xy5Uqt6pPKHmkfmzyPAZL2PCnbRpg8qWYu63I7lu4gswOSRYqrPNt3CgeqqzgbNwRK1A76w76YqjFspfcQTWmK3vJHlQm1puSTVSeqdOc_r9GaeCHfUSY26TXry6Br4RSK3C6yMEGT-aqVU3YbUZ2NF6rfP2KzXgbuYzY46czdgyazy0On_FlLH3F-UDXhgIO35UGlA1rAgAA)): +You might be tempted to do something convoluted with effects to link one value to another. The following example shows two inputs for "money spent" and "money left" that are connected to each other. If you update one, the other should update accordingly. Instead of using effects for this... + ```svelte + + + +``` ```svelte @@ -163,6 +179,7 @@ The fallback value of a prop not declared with `$bindable` is left untouched — clicks: {object.count} ``` + In summary: don't mutate props. Either use callback props to communicate changes, or — if parent and child should share the same object — use the [`$bindable`]($bindable) rune. diff --git a/documentation/docs/02-runes/07-$inspect.md b/documentation/docs/02-runes/07-$inspect.md index f67e250b45..00857f3ef4 100644 --- a/documentation/docs/02-runes/07-$inspect.md +++ b/documentation/docs/02-runes/07-$inspect.md @@ -5,9 +5,11 @@ tags: rune-inspect > [!NOTE] `$inspect` only works during development. In a production build it becomes a noop. -The `$inspect` rune is roughly equivalent to `console.log`, with the exception that it will re-run whenever its argument changes. `$inspect` tracks reactive state deeply, meaning that updating something inside an object or array using fine-grained reactivity will cause it to re-fire ([demo](/playground/untitled#H4sIAAAAAAAACkWQ0YqDQAxFfyUMhSotdZ-tCvu431AXtGOqQ2NmmMm0LOK_r7Utfby5JzeXTOpiCIPKT5PidkSVq2_n1F7Jn3uIcEMSXHSw0evHpAjaGydVzbUQCmgbWaCETZBWMPlKj29nxBDaHj_edkAiu12JhdkYDg61JGvE_s2nR8gyuBuiJZuDJTyQ7eE-IEOzog1YD80Lb0APLfdYc5F9qnFxjiKWwbImo6_llKRQVs-2u91c_bD2OCJLkT3JZasw7KLA2XCX31qKWE6vIzNk1fKE0XbmYrBTufiI8-_8D2cUWBA_AQAA)): +The `$inspect` rune is roughly equivalent to `console.log`, with the exception that it will re-run whenever its argument changes. `$inspect` tracks reactive state deeply, meaning that updating something inside an object or array using fine-grained reactivity will cause it to re-fire: + ```svelte + @@ -71,6 +73,7 @@ Snippets can be declared anywhere inside your component. They can reference valu {@render hello('alice')} {@render hello('bob')} ``` + ...and they are 'visible' to everything in the same lexical scope (i.e. siblings, and children of those siblings): @@ -91,9 +94,11 @@ Snippets can be declared anywhere inside your component. They can reference valu {@render x()} ``` -Snippets can reference themselves and each other ([demo](/playground/untitled#H4sIAAAAAAAAE2WPTQqDMBCFrxLiRqH1Zysi7TlqF1YnENBJSGJLCYGeo5tesUeosfYH3c2bee_jjaWMd6BpfrAU6x5oTvdS0g01V-mFPkNnYNRaDKrxGxto5FKCIaeu1kYwFkauwsoUWtZYPh_3W5FMY4U2mb3egL9kIwY0rbhgiO-sDTgjSEqSTvIDs-jiOP7i_MHuFGAL6p9BtiSbOTl0GtzCuihqE87cqtyam6WRGz_vRcsZh5bmRg3gju4Fptq_kzQBAAA=)): +Snippets can reference themselves and each other: + ```svelte + {#snippet blastoff()} 🚀 {/snippet} @@ -109,14 +114,17 @@ Snippets can reference themselves and each other ([demo](/playground/untitled#H4 {@render countdown(10)} ``` + ## Passing snippets to components ### Explicit props -Within the template, snippets are values just like any other. As such, they can be passed to components as props ([demo](/playground/untitled#H4sIAAAAAAAAE3VS247aMBD9lZGpBGwDASRegonaPvQL2qdlH5zYEKvBNvbQLbL875VzAcKyj3PmzJnLGU8UOwqSkd8KJdaCk4TsZS0cyV49wYuJuQiQpGd-N2bu_ooaI1YwJ57hpVYoFDqSEepKKw3mO7VDeTTaIvxiRS1gb_URxvO0ibrS8WanIrHUyiHs7Vmigy28RmyHHmKvDMbMmFq4cQInvGSwTsBYWYoMVhCSB2rBFFPsyl0uruTlR3JZCWvlTXl1Yy_mawiR_rbZKZrellJ-5JQ0RiBUgnFhJ9OGR7HKmwVoilXeIye8DOJGfYCgRlZ3iE876TBsZPX7hPdteO75PC4QaIo8vwNPePmANQ2fMeEFHrLD7rR1jTNkW986E8C3KwfwVr8HSHOSEBT_kGRozyIkn_zQveXDL3rIfPJHtUDwzShJd_Qk3gQCbOGLsdq4yfTRJopRuin3I7nv6kL7ARRjmLdBDG3uv1mhuLA3V2mKtqNEf_oCn8p9aN-WYqH5peP4kWBl1UwJzAEPT9U7K--0fRrrWnPTXpCm1_EVdXjpNmlA8G1hPPyM1fKgMqjFHjctXGjLhZ05w0qpDhksGrybuNEHtJnCalZWsuaTlfq6nPaaBSv_HKw-K57BjzOiVj9ZKQYKzQjZodYFqydYTRN4gPhVzTDO2xnma3HsVWjaLjT8nbfwHy7Q5f2dBAAA)): +Within the template, snippets are values just like any other. As such, they can be passed to components as props: + ```svelte + + + + {#if header} + + {@render header()} + + {/if} + + + {#each data as d} + {@render row(d)} + {/each} + +
+ + ``` + Think about it like passing content instead of data to a component. The concept is similar to slots in web components. ### Implicit props -As an authoring convenience, snippets declared directly _inside_ a component implicitly become props _on_ the component ([demo](/playground/untitled#H4sIAAAAAAAAE3VSTa_aMBD8Kyu_SkAbCA-JSzBR20N_QXt6vIMTO8SqsY29tI2s_PcqTiB8vaPHs7MzuxuIZgdBMvJLo0QlOElIJZXwJHsLBBvb_XUASc7Mb9Yu_B-hsMMK5sUzvDQahUZPMkJ96aTFfKd3KA_WOISfrFACKmcOMFmk8TWUTjY73RFLoz1C5U4SPWzhrcN2GKDrlcGEWauEnyRwxCaDdQLWyVJksII2uaMWTDPNLtzX5YX8-kgua-GcHJVXI3u5WEPb0d83O03TMZSmfRzOkG1Db7mNacOL19JagVALxoWbztq-H8U6j0SaYp2P2BGbOyQ2v8PQIFMXLKRDk177pq0zf6d8bMrzwBdd0pamyPMb-IjNEzS2f86Gz_Dwf-2F9nvNSUJQ_EOSoTuJNvngqK5v4Pas7n4-OCwlEEJcQTIMO-nSQwtb-GSdsX46e9gbRoP9yGQ11I0rEuycunu6PHx1QnPhxm3SFN15MOlYEFJZtf0dUywMbwZOeBGsrKNLYB54-1R9WNqVdki7usim6VmQphf7mnpshiQRhNAXdoOfMyX3OgMlKtz0cGEcF27uLSul3mewjPjgOOoDukxjPS9rqfh0pb-8zs6aBSt_7505aZ7B9xOi0T9YKW4UooVsr0zB1BTrWQJ3EL-oWcZ572GxFoezCk37QLe3897-B2i2U62uBAAA)): +As an authoring convenience, snippets declared directly _inside_ a component implicitly become props _on_ the component: + ```svelte - + + + {#snippet header()} @@ -169,12 +225,54 @@ As an authoring convenience, snippets declared directly _inside_ a component imp
fruit
``` +```svelte + + + + + {#if header} + + {@render header()} + + {/if} + + + {#each data as d} + {@render row(d)} + {/each} + +
+ + +``` + + ### Implicit `children` snippet -Any content inside the component tags that is _not_ a snippet declaration implicitly becomes part of the `children` snippet ([demo](/playground/untitled#H4sIAAAAAAAAE3WOQQrCMBBFrzIMggql3ddY1Du4si5sOmIwnYRkFKX07lKqglqX8_7_w2uRDw1hjlsWI5ZqTPBoLEXMdy3K3fdZDzB5Ndfep_FKVnpWHSKNce1YiCVijirqYLwUJQOYxrsgsLmIOIZjcA1M02w4n-PpomSVvTclqyEutDX6DA2pZ7_ABIVugrmEC3XJH92P55_G39GodCmWBFrQJ2PrQAwdLGHig_NxNv9xrQa1dhWIawrv1Wzeqawa8953D-8QOmaEAQAA)): +Any content inside the component tags that is _not_ a snippet declaration implicitly becomes part of the `children` snippet: + ```svelte + + ``` @@ -187,6 +285,7 @@ Any content inside the component tags that is _not_ a snippet declaration implic ``` + > [!NOTE] Note that you cannot have a prop called `children` if you also have content inside the component — for this reason, you should avoid having props with that name @@ -256,9 +355,21 @@ We can tighten things up further by declaring a generic, so that `data` and `row ## Exporting snippets -Snippets declared at the top level of a `.svelte` file can be exported from a ` + +{@render add(1, 2)} + +``` ```svelte + @@ -267,6 +378,7 @@ Snippets declared at the top level of a `.svelte` file can be exported from a `< {a} + {b} = {a + b} {/snippet} ``` + > [!NOTE] > This requires Svelte 5.5.0 or newer diff --git a/documentation/docs/03-template-syntax/12-bind.md b/documentation/docs/03-template-syntax/12-bind.md index be84969b87..e8164149db 100644 --- a/documentation/docs/03-template-syntax/12-bind.md +++ b/documentation/docs/03-template-syntax/12-bind.md @@ -54,9 +54,11 @@ A `bind:value` directive on an `` element binds the input's `value` prope

{message}

``` -In the case of a numeric input (`type="number"` or `type="range"`), the value will be coerced to a number ([demo](/playground/untitled#H4sIAAAAAAAAE6WPwYoCMQxAfyWEPeyiOOqx2w74Hds9pBql0IllmhGXYf5dKqwiyILsLXnwwsuI-5i4oPkaUX8yo7kCnKNQV7dNzoty4qSVBSr8jG-Poixa0KAt2z5mbb14TaxA4OCtKCm_rz4-f2m403WltrlrYhMFTtcLNkoeFGqZ8yhDF7j3CCHKzpwoDexGmqCL4jwuPUJHZ-dxVcfmyYGe5MAv-La5pbxYFf5Z9Zf_UJXb-sEMquFgJJhBmGyTW5yj8lnRaD_w9D1dAKSSj7zqAQAA)): +In the case of a numeric input (`type="number"` or `type="range"`), the value will be coerced to a number: + ```svelte + +

Customize your burrito

+ @@ -165,7 +171,17 @@ Inputs that work together can use `bind:group` ([demo](/playground/untitled#H4sI + +

Tortilla: {tortilla}

+

Fillings: {fillings.join(', ') || 'None'}

+ + ``` + > [!NOTE] `bind:group` only works if the inputs are in the same Svelte component. diff --git a/documentation/docs/03-template-syntax/17-style.md b/documentation/docs/03-template-syntax/17-style.md index 8b25c221d6..6ddb128f4a 100644 --- a/documentation/docs/03-template-syntax/17-style.md +++ b/documentation/docs/03-template-syntax/17-style.md @@ -42,3 +42,9 @@ even over `!important` properties:
This will be red
This will still be red
``` + +You can set CSS custom properties: + +```svelte +
...
+``` diff --git a/documentation/docs/03-template-syntax/19-await-expressions.md b/documentation/docs/03-template-syntax/19-await-expressions.md index 2f73f6a47c..04437f5ee3 100644 --- a/documentation/docs/03-template-syntax/19-await-expressions.md +++ b/documentation/docs/03-template-syntax/19-await-expressions.md @@ -25,9 +25,11 @@ The experimental flag will be removed in Svelte 6. ## Synchronized updates -When an `await` expression depends on a particular piece of state, changes to that state will not be reflected in the UI until the asynchronous work has completed, so that the UI is not left in an inconsistent state. In other words, in an example like [this](/playground/untitled#H4sIAAAAAAAAE42QsWrDQBBEf2VZUkhYRE4gjSwJ0qVMkS6XYk9awcFpJe5Wdoy4fw-ycdykSPt2dpiZFYVGxgrf2PsJTlPwPWTcO-U-xwIH5zli9bminudNtwEsbl-v8_wYj-x1Y5Yi_8W7SZRFI1ZYxy64WVsjRj0rEDTwEJWUs6f8cKP2Tp8vVIxSPEsHwyKdukmA-j6jAmwO63Y1SidyCsIneA_T6CJn2ZBD00Jk_XAjT4tmQwEv-32eH6AsgYK6wXWOPPTs6Xy1CaxLECDYgb3kSUbq8p5aaifzorCt0RiUZbQcDIJ10ldH8gs3K6X2Xzqbro5zu1KCHaw2QQPrtclvwVSXc2sEC1T-Vqw0LJy-ClRy_uSkx2ogHzn9ADZ1CubKAQAA)... +When an `await` expression depends on a particular piece of state, changes to that state will not be reflected in the UI until the asynchronous work has completed, so that the UI is not left in an inconsistent state. In other words, in an example like this... + ```svelte + + + + + +``` + +```svelte + + + +{@render children()} +``` + +```svelte + + + +

hello {user.name}, inside Child.svelte

+``` + +```ts +/// file: context.ts +import { createContext } from 'svelte'; + +interface User { + name: string; +} + +export const [getUserContext, setUserContext] = createContext(); +``` + + +> [!NOTE] `createContext` was added in version 5.40. If you are using an earlier version of Svelte, you must use `setContext` and `getContext` instead. + +This is particularly useful when `Parent.svelte` is not directly aware of `Child.svelte`, but instead renders it as part of a `children` [snippet](snippet) as shown above. + +## `setContext` and `getContext` + +As an alternative to `createContext`, you can use `setContext` and `getContext` directly. The parent component sets context with `setContext(key, value)`... ```svelte @@ -26,32 +85,28 @@ Context allows components to access values owned by parent components without pa

{message}, inside Child.svelte

``` -This is particularly useful when `Parent.svelte` is not directly aware of `Child.svelte`, but instead renders it as part of a `children` [snippet](snippet) ([demo](/playground/untitled#H4sIAAAAAAAAE42Q3W6DMAyFX8WyJgESK-oto6hTX2D3YxcM3IIUQpR40yqUd58CrCXsp7tL7HNsf2dAWXaEKR56yfTBGOOxFWQwfR6Qz8q1XAHjL-GjUhvzToJd7bU09FO9ctMkG0wxM5VuFeeFLLjtVK8ZnkpNkuGo-w6CTTJ9Z3PwsBAemlbUF934W8iy5DpaZtOUcU02-ZLcaS51jHEkTFm_kY1_wfOO8QnXrb8hBzDEc6pgZ4gFoyz4KgiD7nxfTe8ghqAhIfrJ46cTzVZBbkPlODVJsLCDO6V7ZcJoncyw1yRr0hd1GNn_ZbEM3I9i1bmVxOlWElUvDUNHxpQngt3C4CXzjS1rtvkw22wMrTRtTbC8Lkuabe7jvthPPe3DofYCAAA=)): - -```svelte - - - -``` - The key (`'my-context'`, in the example above) and the context itself can be any JavaScript value. +> [!NOTE] `createContext` is preferred since it provides better type safety and makes it unnecessary to use keys. + In addition to [`setContext`](svelte#setContext) and [`getContext`](svelte#getContext), Svelte exposes [`hasContext`](svelte#hasContext) and [`getAllContexts`](svelte#getAllContexts) functions. ## Using context with state -You can store reactive state in context ([demo](/playground/untitled#H4sIAAAAAAAAE41R0W6DMAz8FSuaBNUQdK8MkKZ-wh7HHihzu6hgosRMm1D-fUpSVNq12x4iEvvOx_kmQU2PIhfP3DCCJGgHYvxkkYid7NCI_GUS_KUcxhVEMjOelErNB3bsatvG4LW6n0ZsRC4K02qpuKqpZtmrQTNMYJA3QRAs7PTQQxS40eMCt3mX3duxnWb-lS5h7nTI0A4jMWoo4c44P_Hku-zrOazdy64chWo-ScfRkRgl8wgHKrLTH1OxHZkHgoHaTraHcopXUFYzPPVfuC_hwQaD1GrskdiNCdQwJljJqlvXfyqVsA5CGg0uRUQifHw56xFtciO75QrP07vo_JXf_tf8yK2ezDKY_ZWt_1y2qqYzv7bI1IW1V_sN19m-07wCAAA=))... +You can store reactive state in context... + ```svelte + +``` + +```svelte + + + +

{counter.count}

+``` + +```ts +/// file: context.ts +import { createContext } from 'svelte'; + +interface Counter { + count: number; +} + +export const [getCounter, setCounter] = createContext(); ``` + ...though note that if you _reassign_ `counter` instead of updating it, you will 'break the link' — in other words instead of this... ```svelte - ``` @@ -81,21 +163,7 @@ You can store reactive state in context ([demo](/playground/untitled#H4sIAAAAAAA Svelte will warn you if you get it wrong. -## Type-safe context - -As an alternative to using `setContext` and `getContext` directly, you can use them via `createContext`. This gives you type safety and makes it unnecessary to use a key: - -```ts -/// file: context.ts -// @filename: ambient.d.ts -interface User {} - -// @filename: index.ts -// ---cut--- -import { createContext } from 'svelte'; - -export const [getUserContext, setUserContext] = createContext(); -``` +## Component testing When writing [component tests](testing#Unit-and-component-tests-with-Vitest-Component-testing), it can be useful to create a wrapper component that sets the context in order to check the behaviour of a component that uses it. As of version 5.49, you can do this sort of thing: @@ -140,7 +208,7 @@ export const myGlobalState = $state({ In many cases this is perfectly fine, but there is a risk: if you mutate the state during server-side rendering (which is discouraged, but entirely possible!)... ```svelte - + * ``` */ - static of(fn: () => U, options?: SpringOpts): Spring; + static of(fn: () => U, options?: SpringOptions): Spring; /** * Sets `spring.target` to `value` and returns a `Promise` that resolves if and when `spring.current` catches up to it. @@ -63,7 +96,7 @@ export class Spring { * If `options.preserveMomentum` is provided, the spring will continue on its current trajectory for * the specified number of milliseconds. This is useful for things like 'fling' gestures. */ - set(value: T, options?: SpringUpdateOpts): Promise; + set(value: T, options?: SpringUpdateOptions): Promise; damping: number; precision: number; @@ -81,8 +114,8 @@ export class Spring { } export interface Tweened extends Readable { - set(value: T, opts?: TweenedOptions): Promise; - update(updater: Updater, opts?: TweenedOptions): Promise; + set(value: T, opts?: TweenOptions): Promise; + update(updater: Updater, opts?: TweenOptions): Promise; } export { prefersReducedMotion, spring, tweened, Tween } from './index.js'; diff --git a/packages/svelte/src/motion/spring.js b/packages/svelte/src/motion/spring.js index 44be1a501b..7198d60127 100644 --- a/packages/svelte/src/motion/spring.js +++ b/packages/svelte/src/motion/spring.js @@ -1,6 +1,6 @@ /** @import { Task } from '#client' */ -/** @import { SpringOpts, SpringUpdateOpts, TickContext } from './private.js' */ -/** @import { Spring as SpringStore } from './public.js' */ +/** @import { TickContext } from './private.js' */ +/** @import { Spring as SpringStore, SpringOptions, SpringUpdateOptions } from './public.js' */ import { writable } from '../store/shared/index.js'; import { loop } from '../internal/client/loop.js'; import { raf } from '../internal/client/timing.js'; @@ -62,7 +62,7 @@ function tick_spring(ctx, last_value, current_value, target_value) { * @deprecated Use [`Spring`](https://svelte.dev/docs/svelte/svelte-motion#Spring) instead * @template [T=any] * @param {T} [value] - * @param {SpringOpts} [opts] + * @param {SpringOptions} [opts] * @returns {SpringStore} */ export function spring(value, opts = {}) { @@ -83,7 +83,7 @@ export function spring(value, opts = {}) { let cancel_task = false; /** * @param {T} new_value - * @param {SpringUpdateOpts} opts + * @param {SpringUpdateOptions} opts * @returns {Promise} */ function set(new_value, opts = {}) { @@ -191,7 +191,7 @@ export class Spring { /** * @param {T} value - * @param {SpringOpts} [options] + * @param {SpringOptions} [options] */ constructor(value, options = {}) { this.#current = DEV ? tag(state(value), 'Spring.current') : state(value); @@ -225,7 +225,7 @@ export class Spring { * ``` * @template U * @param {() => U} fn - * @param {SpringOpts} [options] + * @param {SpringOptions} [options] */ static of(fn, options) { const spring = new Spring(fn(), options); @@ -293,7 +293,7 @@ export class Spring { * the specified number of milliseconds. This is useful for things like 'fling' gestures. * * @param {T} value - * @param {SpringUpdateOpts} [options] + * @param {SpringUpdateOptions} [options] */ set(value, options) { this.#deferred?.reject(new Error('Aborted')); diff --git a/packages/svelte/src/motion/tweened.js b/packages/svelte/src/motion/tweened.js index 437c22ec3b..a24148d075 100644 --- a/packages/svelte/src/motion/tweened.js +++ b/packages/svelte/src/motion/tweened.js @@ -1,6 +1,5 @@ /** @import { Task } from '../internal/client/types' */ -/** @import { Tweened } from './public' */ -/** @import { TweenedOptions } from './private' */ +/** @import { Tweened, TweenOptions } from './public' */ import { writable } from '../store/shared/index.js'; import { raf } from '../internal/client/timing.js'; import { loop } from '../internal/client/loop.js'; @@ -84,7 +83,7 @@ function get_interpolator(a, b) { * @deprecated Use [`Tween`](https://svelte.dev/docs/svelte/svelte-motion#Tween) instead * @template T * @param {T} [value] - * @param {TweenedOptions} [defaults] + * @param {TweenOptions} [defaults] * @returns {Tweened} */ export function tweened(value, defaults = {}) { @@ -94,7 +93,7 @@ export function tweened(value, defaults = {}) { let target_value = value; /** * @param {T} new_value - * @param {TweenedOptions} [opts] + * @param {TweenOptions} [opts] */ function set(new_value, opts) { target_value = new_value; @@ -180,7 +179,7 @@ export class Tween { #current; #target; - /** @type {TweenedOptions} */ + /** @type {TweenOptions} */ #defaults; /** @type {import('../internal/client/types').Task | null} */ @@ -188,7 +187,7 @@ export class Tween { /** * @param {T} value - * @param {TweenedOptions} options + * @param {TweenOptions} options */ constructor(value, options = {}) { this.#current = state(value); @@ -216,7 +215,7 @@ export class Tween { * ``` * @template U * @param {() => U} fn - * @param {TweenedOptions} [options] + * @param {TweenOptions} [options] */ static of(fn, options) { const tween = new Tween(fn(), options); @@ -233,7 +232,7 @@ export class Tween { * * If `options` are provided, they will override the tween's defaults. * @param {T} value - * @param {TweenedOptions} [options] + * @param {TweenOptions} [options] * @returns */ set(value, options) { diff --git a/packages/svelte/src/reactivity/date.js b/packages/svelte/src/reactivity/date.js index 8e2b5d41ab..f882c05d76 100644 --- a/packages/svelte/src/reactivity/date.js +++ b/packages/svelte/src/reactivity/date.js @@ -95,6 +95,10 @@ export class SvelteDate extends Date { return date_proto[method].apply(this, args); }); + if (DEV) { + tag(d, `SvelteDate.${method}()`); + } + this.#deriveds.set(method, d); set_active_reaction(reaction); diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index 191d53016a..f168aadee1 100644 --- a/packages/svelte/src/version.js +++ b/packages/svelte/src/version.js @@ -4,5 +4,5 @@ * The current version, as set in package.json. * @type {string} */ -export const VERSION = '5.54.0'; +export const VERSION = '5.55.1'; export const PUBLIC_VERSION = '5'; diff --git a/packages/svelte/tests/hydration/samples/css-props-hmr/Component.svelte b/packages/svelte/tests/hydration/samples/css-props-hmr/Component.svelte new file mode 100644 index 0000000000..ac73501cca --- /dev/null +++ b/packages/svelte/tests/hydration/samples/css-props-hmr/Component.svelte @@ -0,0 +1,7 @@ +

Hello

+ + \ No newline at end of file diff --git a/packages/svelte/tests/hydration/samples/css-props-hmr/_config.js b/packages/svelte/tests/hydration/samples/css-props-hmr/_config.js new file mode 100644 index 0000000000..2cd696d584 --- /dev/null +++ b/packages/svelte/tests/hydration/samples/css-props-hmr/_config.js @@ -0,0 +1,7 @@ +import { test } from '../../test'; + +export default test({ + compileOptions: { + hmr: true + } +}); diff --git a/packages/svelte/tests/hydration/samples/css-props-hmr/main.svelte b/packages/svelte/tests/hydration/samples/css-props-hmr/main.svelte new file mode 100644 index 0000000000..eed9b57caf --- /dev/null +++ b/packages/svelte/tests/hydration/samples/css-props-hmr/main.svelte @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/packages/svelte/tests/print/samples/const-tag/output.svelte b/packages/svelte/tests/print/samples/const-tag/output.svelte index d9d8addcbb..dded998a84 100644 --- a/packages/svelte/tests/print/samples/const-tag/output.svelte +++ b/packages/svelte/tests/print/samples/const-tag/output.svelte @@ -3,6 +3,6 @@ {#each boxes as box} - {@const area = box.width * box.height;} + {@const area = box.width * box.height} {box.width} * {box.height} = {area} {/each} diff --git a/packages/svelte/tests/runtime-legacy/samples/const-tag-depends-on-const-tag/_config.js b/packages/svelte/tests/runtime-legacy/samples/const-tag-depends-on-const-tag/_config.js new file mode 100644 index 0000000000..6282d36f75 --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/const-tag-depends-on-const-tag/_config.js @@ -0,0 +1,34 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: ` + +

hello

+

hello

+ `, + + ssrHtml: ` + +

hello

+

hello

+ `, + + async test({ assert, target }) { + const [input] = target.querySelectorAll('input'); + + flushSync(() => { + input.value = 'goodbye'; + input.dispatchEvent(new InputEvent('input', { bubbles: true })); + }); + + assert.htmlEqual( + target.innerHTML, + ` + +

goodbye

+

goodbye

+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-legacy/samples/const-tag-depends-on-const-tag/main.svelte b/packages/svelte/tests/runtime-legacy/samples/const-tag-depends-on-const-tag/main.svelte new file mode 100644 index 0000000000..a32010717c --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/const-tag-depends-on-const-tag/main.svelte @@ -0,0 +1,15 @@ + + + + + + +{#if true} + {@const m1 = message} + {@const m2 = (() => m1)()} + +

{m1}

+

{m2}

+{/if} diff --git a/packages/svelte/tests/runtime-legacy/samples/flush-sync-each-block/Inner.svelte b/packages/svelte/tests/runtime-legacy/samples/flush-sync-each-block/Inner.svelte new file mode 100644 index 0000000000..6d7c982db0 --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/flush-sync-each-block/Inner.svelte @@ -0,0 +1,8 @@ + + +{value} diff --git a/packages/svelte/tests/runtime-legacy/samples/flush-sync-each-block/_config.js b/packages/svelte/tests/runtime-legacy/samples/flush-sync-each-block/_config.js new file mode 100644 index 0000000000..2798f533c6 --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/flush-sync-each-block/_config.js @@ -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, + ` + + + 2 + ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-legacy/samples/flush-sync-each-block/main.svelte b/packages/svelte/tests/runtime-legacy/samples/flush-sync-each-block/main.svelte new file mode 100644 index 0000000000..3be1202c97 --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/flush-sync-each-block/main.svelte @@ -0,0 +1,12 @@ + + + + + + +{#each [count] as row} + {row} +{/each} diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-indirect/_config.js b/packages/svelte/tests/runtime-runes/samples/async-derived-indirect/_config.js new file mode 100644 index 0000000000..9d97b736be --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-derived-indirect/_config.js @@ -0,0 +1,29 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + await tick(); + const [a, b, resolve] = target.querySelectorAll('button'); + + a.click(); + await tick(); + b.click(); + await tick(); + resolve.click(); + await tick(); + resolve.click(); + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` + + + + hi + 1 + ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-indirect/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-derived-indirect/main.svelte new file mode 100644 index 0000000000..f0d3bdc809 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-derived-indirect/main.svelte @@ -0,0 +1,21 @@ + + + + + + +{#if a_b}hi{/if} +{await push(a_b)} diff --git a/packages/svelte/tests/runtime-runes/samples/async-each-overlap/_config.js b/packages/svelte/tests/runtime-runes/samples/async-each-overlap/_config.js deleted file mode 100644 index d03f9ad09d..0000000000 --- a/packages/svelte/tests/runtime-runes/samples/async-each-overlap/_config.js +++ /dev/null @@ -1,102 +0,0 @@ -import { tick } from 'svelte'; -import { test } from '../../test'; - -export default test({ - skip: true, - async test({ assert, target }) { - const [add, shift] = target.querySelectorAll('button'); - - add.click(); - await tick(); - add.click(); - await tick(); - add.click(); - await tick(); - - // TODO pending count / number of pushes is off - - assert.htmlEqual( - target.innerHTML, - ` - - -

pending=6 values.length=1 values=[1]

-
not keyed: -
1
-
-
keyed: -
1
-
- ` - ); - - shift.click(); - await tick(); - shift.click(); - await tick(); - assert.htmlEqual( - target.innerHTML, - ` - - -

pending=4 values.length=2 values=[1,2]

-
not keyed: -
1
-
2
-
-
keyed: -
1
-
2
-
- ` - ); - - shift.click(); - await tick(); - shift.click(); - await tick(); - assert.htmlEqual( - target.innerHTML, - ` - - -

pending=2 values.length=3 values=[1,2,3]

-
not keyed: -
1
-
2
-
3
-
-
keyed: -
1
-
2
-
3
-
- ` - ); - - shift.click(); - await tick(); - shift.click(); - await tick(); - assert.htmlEqual( - target.innerHTML, - ` - - -

pending=0 values.length=4 values=[1,2,3,4]

-
not keyed: -
1
-
2
-
3
-
4
-
-
keyed: -
1
-
2
-
3
-
4
-
- ` - ); - } -}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-fork-if-derived/_config.js b/packages/svelte/tests/runtime-runes/samples/async-fork-if-derived/_config.js new file mode 100644 index 0000000000..4a03a54e7a --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-fork-if-derived/_config.js @@ -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, ` 0`); + + toggle?.click(); + await tick(); + assert.htmlEqual(target.innerHTML, ` `); + + toggle?.click(); + await tick(); + assert.htmlEqual(target.innerHTML, ` 0`); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-fork-if-derived/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-fork-if-derived/main.svelte new file mode 100644 index 0000000000..519857a9ce --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-fork-if-derived/main.svelte @@ -0,0 +1,13 @@ + + + + + +{#if show} + {d_count} +{/if} diff --git a/packages/svelte/tests/runtime-runes/samples/async-fork-resolves-promise/_config.js b/packages/svelte/tests/runtime-runes/samples/async-fork-resolves-promise/_config.js new file mode 100644 index 0000000000..ff323b60af --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-fork-resolves-promise/_config.js @@ -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'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-fork-resolves-promise/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-fork-resolves-promise/main.svelte new file mode 100644 index 0000000000..d6995340af --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-fork-resolves-promise/main.svelte @@ -0,0 +1,21 @@ + + +

{x} {await delay(y)}

+ + + + + diff --git a/packages/svelte/tests/runtime-runes/samples/async-hydrate-html-tag/_config.js b/packages/svelte/tests/runtime-runes/samples/async-hydrate-html-tag/_config.js new file mode 100644 index 0000000000..cb73ab1833 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-hydrate-html-tag/_config.js @@ -0,0 +1,10 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + mode: ['hydrate'], + async test({ assert, target }) { + await tick(); + assert.htmlEqual(target.innerHTML, `

first test

other test
`); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-hydrate-html-tag/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-hydrate-html-tag/main.svelte new file mode 100644 index 0000000000..cd34a1a16e --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-hydrate-html-tag/main.svelte @@ -0,0 +1,14 @@ + + +
+
{@html await firstTest()}
+ {await otherTest()} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-hydration-binding/Async.svelte b/packages/svelte/tests/runtime-runes/samples/async-hydration-binding/Async.svelte new file mode 100644 index 0000000000..6e7dbc82bb --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-hydration-binding/Async.svelte @@ -0,0 +1,7 @@ + + +
+ {data} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-hydration-binding/Binding.svelte b/packages/svelte/tests/runtime-runes/samples/async-hydration-binding/Binding.svelte new file mode 100644 index 0000000000..3deba21acc --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-hydration-binding/Binding.svelte @@ -0,0 +1,7 @@ + + + diff --git a/packages/svelte/tests/runtime-runes/samples/async-hydration-binding/Bound.svelte b/packages/svelte/tests/runtime-runes/samples/async-hydration-binding/Bound.svelte new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/svelte/tests/runtime-runes/samples/async-hydration-binding/_config.js b/packages/svelte/tests/runtime-runes/samples/async-hydration-binding/_config.js new file mode 100644 index 0000000000..d4d36b007f --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-hydration-binding/_config.js @@ -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: '
test
', + async test({ assert, warnings }) { + assert.deepEqual(warnings, []); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-hydration-binding/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-hydration-binding/main.svelte new file mode 100644 index 0000000000..a38806be7c --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-hydration-binding/main.svelte @@ -0,0 +1,7 @@ + + + + diff --git a/packages/svelte/tests/runtime-runes/samples/async-if-after-await-in-script/_config.js b/packages/svelte/tests/runtime-runes/samples/async-if-after-await-in-script/_config.js index 2b8ab6e894..1003d3da05 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-if-after-await-in-script/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-if-after-await-in-script/_config.js @@ -6,11 +6,13 @@ export default test({ ssrHtml: '

yep

', - async test({ assert, target, variant }) { + async test({ assert, target, variant, warnings }) { if (variant === 'dom') { await tick(); } assert.htmlEqual(target.innerHTML, '

yep

'); + + assert.deepEqual(warnings, []); // TODO not quite sure why this isn't populated yet } }); diff --git a/packages/svelte/tests/runtime-runes/samples/async-if-block-unskip/_config.js b/packages/svelte/tests/runtime-runes/samples/async-if-block-unskip/_config.js new file mode 100644 index 0000000000..d578def483 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-if-block-unskip/_config.js @@ -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, + ` + + + ` + ); + + resolve.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + search search search search + + + ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-if-block-unskip/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-if-block-unskip/main.svelte new file mode 100644 index 0000000000..36ebcd26d1 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-if-block-unskip/main.svelte @@ -0,0 +1,40 @@ + + +{query} {await promise} + +{#if !promise.loading} + {query} +{/if} + +{#if !promise.loading} + {await query} +{/if} + + + diff --git a/packages/svelte/tests/runtime-runes/samples/async-ignore-skipped-block/_config.js b/packages/svelte/tests/runtime-runes/samples/async-ignore-skipped-block/_config.js new file mode 100644 index 0000000000..1d1a489d67 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-ignore-skipped-block/_config.js @@ -0,0 +1,29 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + await tick(); + + const [a, b] = target.querySelectorAll('button'); + + assert.htmlEqual(target.innerHTML, `

hello

`); + + a.click(); + await tick(); + assert.htmlEqual(target.innerHTML, `

hello

`); + + a.click(); + await tick(); + assert.htmlEqual(target.innerHTML, `

hello

`); + + a.click(); + await tick(); + assert.htmlEqual(target.innerHTML, `

hello

`); + + // if we don't skip over the never-resolving promise in the `else` block, we will never update + b.click(); + await tick(); + assert.htmlEqual(target.innerHTML, `

hello

`); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-ignore-skipped-block/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-ignore-skipped-block/main.svelte new file mode 100644 index 0000000000..4ef0694d40 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-ignore-skipped-block/main.svelte @@ -0,0 +1,14 @@ + + + + + +{#if show} +

hello

+{:else} + {await new Promise(() => {})} +{/if} diff --git a/packages/svelte/tests/runtime-runes/samples/async-later-sync-overlaps/_config.js b/packages/svelte/tests/runtime-runes/samples/async-later-sync-overlaps/_config.js new file mode 100644 index 0000000000..a65c8a6fba --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-later-sync-overlaps/_config.js @@ -0,0 +1,30 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + await tick(); + const [a_b, b, resolve] = target.querySelectorAll('button'); + + a_b.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ' 0' + ); + + b.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ' 0' + ); + + resolve.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ' 1' + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-later-sync-overlaps/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-later-sync-overlaps/main.svelte new file mode 100644 index 0000000000..8a9e2a866c --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-later-sync-overlaps/main.svelte @@ -0,0 +1,17 @@ + + + + + +{await push(a)} diff --git a/packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-1/_config.js b/packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-1/_config.js new file mode 100644 index 0000000000..03d8e49b6f --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-1/_config.js @@ -0,0 +1,107 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + await tick(); + const [a_b, a_c, b_d, shift, pop] = target.querySelectorAll('button'); + + a_b.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + a 0 | b 0 | c 0 | d 0 + + + + + + ` + ); + + a_c.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + a 0 | b 0 | c 0 | d 0 + + + + + + ` + ); + + b_d.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + a 0 | b 0 | c 0 | d 0 + + + + + + ` + ); + + shift.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + a 0 | b 0 | c 0 | d 0 + + + + + + ` + ); + + shift.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + a 1 | b 1 | c 0 | d 0 + + + + + + ` + ); + + shift.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + a 2 | b 1 | c 1 | d 0 + + + + + + ` + ); + + shift.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + a 2 | b 2 | c 1 | d 1 + + + + + + ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-1/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-1/main.svelte new file mode 100644 index 0000000000..25dbe586ca --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-1/main.svelte @@ -0,0 +1,26 @@ + + +a {await delay(a)} | b {await delay(b)} | c {c} | d {d} + + + + + diff --git a/packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-2/_config.js b/packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-2/_config.js new file mode 100644 index 0000000000..f4aff83aad --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-2/_config.js @@ -0,0 +1,82 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + skip: true, // TODO works on https://github.com/sveltejs/svelte/pull/17971 + async test({ assert, target }) { + await tick(); + const [a_b, a_c, b_d, shift, pop] = target.querySelectorAll('button'); + + a_b.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + a 0 | b 0 | c 0 | d 0 + + + + + + ` + ); + + a_c.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + a 0 | b 0 | c 0 | d 0 + + + + + + ` + ); + + b_d.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + a 0 | b 0 | c 0 | d 0 + + + + + + ` + ); + + pop.click(); // second b resolved, blocked on first batch because a still pending + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + a 0 | b 0 | c 0 | d 0 + + + + + + ` + ); + + for (let i = 0; i < 3; i++) { + pop.click(); // second a resolved, first a/b now obsolete; empty queue + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + a 2 | b 2 | c 1 | d 1 + + + + + + ` + ); + } + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-2/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-2/main.svelte new file mode 100644 index 0000000000..25dbe586ca --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-2/main.svelte @@ -0,0 +1,26 @@ + + +a {await delay(a)} | b {await delay(b)} | c {c} | d {d} + + + + + diff --git a/packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-3/_config.js b/packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-3/_config.js new file mode 100644 index 0000000000..c9e7513b22 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-3/_config.js @@ -0,0 +1,108 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + skip: true, // TODO works on https://github.com/sveltejs/svelte/pull/17971 + async test({ assert, target }) { + await tick(); + const [a_b, a_c, b_d, shift, pop] = target.querySelectorAll('button'); + + a_b.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + a 0 | b 0 | c 0 | d 0 + + + + + + ` + ); + + a_c.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + a 0 | b 0 | c 0 | d 0 + + + + + + ` + ); + + b_d.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + a 0 | b 0 | c 0 | d 0 + + + + + + ` + ); + + shift.click(); // first a resolved, still pending: [b, a, b] + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + a 0 | b 0 | c 0 | d 0 + + + + + + ` + ); + + pop.click(); // second b resolved, still pending: [b, a] + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + a 0 | b 0 | c 0 | d 0 + + + + + + ` + ); + + shift.click(); // first b resolved, first + last batch settled, still pending: [a] + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + a 1 | b 2 | c 0 | d 1 + + + + + + ` + ); + + shift.click(); // all resolved + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + a 2 | b 2 | c 1 | d 1 + + + + + + ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-3/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-3/main.svelte new file mode 100644 index 0000000000..25dbe586ca --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-3/main.svelte @@ -0,0 +1,26 @@ + + +a {await delay(a)} | b {await delay(b)} | c {c} | d {d} + + + + + diff --git a/packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-4/_config.js b/packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-4/_config.js new file mode 100644 index 0000000000..472a3ebcaf --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-4/_config.js @@ -0,0 +1,110 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + skip: true, // TODO works on https://github.com/sveltejs/svelte/pull/17971 + async test({ assert, target }) { + await tick(); + const [a_b, a_c, b_d, shift, pop] = target.querySelectorAll('button'); + + a_b.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + a 0 | b 0 | c 0 | d 0 + + + + + + ` + ); + + a_c.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + a 0 | b 0 | c 0 | d 0 + + + + + + ` + ); + + b_d.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + a 0 | b 0 | c 0 | d 0 + + + + + + ` + ); + + shift.click(); // first a resolved, still pending: [b, a, b] + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + a 0 | b 0 | c 0 | d 0 + + + + + + ` + ); + + pop.click(); // second b resolved, still pending: [b, a] + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + a 0 | b 0 | c 0 | d 0 + + + + + + ` + ); + + pop.click(); // second a resolved, first a/b now obsolete + // TODO would be nice to show final result here already, right now it doesn't because + // we have no handle on the already resolved first a anymore + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + a 0 | b 0 | c 0 | d 0 + + + + + + ` + ); + + shift.click(); // queue empty + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + a 2 | b 2 | c 1 | d 1 + + + + + + ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-4/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-4/main.svelte new file mode 100644 index 0000000000..25dbe586ca --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-4/main.svelte @@ -0,0 +1,26 @@ + + +a {await delay(a)} | b {await delay(b)} | c {c} | d {d} + + + + + diff --git a/packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-5/_config.js b/packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-5/_config.js new file mode 100644 index 0000000000..d03f3cfbb8 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-5/_config.js @@ -0,0 +1,118 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + await tick(); + const [a, c, shift, pop] = target.querySelectorAll('button'); + + a.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + a 0 | b 0 | c 0 | d 0 + + + + + ` + ); + + c.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + a 0 | b 0 | c 0 | d 0 + + + + + ` + ); + + shift.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + a 0 | b 0 | c 0 | d 0 + + + + + ` + ); + + // how it's on main + + shift.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + a 0 | b 0 | c 1 | d 1 + + + + + ` + ); + + shift.click(); + await tick(); + shift.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + a 1 | b 2 | c 1 | d 3 + + + + + ` + ); + + // how it's on https://github.com/sveltejs/svelte/pull/17971 + // shift.click(); + // await tick(); + // assert.htmlEqual( + // target.innerHTML, + // ` + // a 0 | b 0 | c 0 | d 0 + // + // + // + // + // ` + // ); + + // shift.click(); + // await tick(); + // assert.htmlEqual( + // target.innerHTML, + // ` + // a 1 | b 2 | c 0 | d 2 + // + // + // + // + // ` + // ); + + // shift.click(); + // await tick(); + // assert.htmlEqual( + // target.innerHTML, + // ` + // a 1 | b 2 | c 1 | d 3 + // + // + // + // + // ` + // ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-5/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-5/main.svelte new file mode 100644 index 0000000000..cf028718c2 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-5/main.svelte @@ -0,0 +1,23 @@ + + +a {a} | b {b} | c {c} | d {d} + + + + \ No newline at end of file diff --git a/packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-6/_config.js b/packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-6/_config.js new file mode 100644 index 0000000000..27152889a2 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-6/_config.js @@ -0,0 +1,76 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + await tick(); + const [a, c, shift, pop] = target.querySelectorAll('button'); + + a.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + a 0 | b 0 | c 0 | d 0 + + + + + ` + ); + + c.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + a 0 | b 0 | c 0 | d 0 + + + + + ` + ); + + // Although the second batch is eventually connected to the first one, we can't see that + // at this point yet and so the second one flushes right away. + pop.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + a 0 | b 0 | c 1 | d 1 + + + + + ` + ); + + pop.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + a 0 | b 0 | c 1 | d 1 + + + + + ` + ); + + pop.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + a 1 | b 2 | c 1 | d 3 + + + + + ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-6/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-6/main.svelte new file mode 100644 index 0000000000..cf028718c2 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-6/main.svelte @@ -0,0 +1,23 @@ + + +a {a} | b {b} | c {c} | d {d} + + + + \ No newline at end of file diff --git a/packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-7/_config.js b/packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-7/_config.js new file mode 100644 index 0000000000..b1bf53a2fc --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-7/_config.js @@ -0,0 +1,129 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + await tick(); + const [a, c, shift, pop] = target.querySelectorAll('button'); + + a.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + a 0 | b 0 | c 0 | d 0 + + + + + ` + ); + + c.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + a 0 | b 0 | c 0 | d 0 + + + + + ` + ); + + shift.click(); // schedules second step of first batch and schedules rerun of second batch + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + a 0 | b 0 | c 0 | d 0 + + + + + ` + ); + + // how it's on main + + pop.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + a 1 | b 2 | c 0 | d 2 + + + + + ` + ); + + shift.click(); // obsolete second batch promise (already rejected) + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + a 1 | b 2 | c 0 | d 2 + + + + + ` + ); + + shift.click(); // first batch resolves + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + a 1 | b 2 | c 1 | d 3 + + + + + ` + ); + + // how it's on https://github.com/sveltejs/svelte/pull/17971 + // pop.click(); // second batch resolves but knows it needs to wait on first batch + // await tick(); + // assert.htmlEqual( + // target.innerHTML, + // ` + // a 0 | b 0 | c 0 | d 0 + // + // + // + // + // ` + // ); + + // shift.click(); // obsolete second batch promise (already rejected) + // await tick(); + // assert.htmlEqual( + // target.innerHTML, + // ` + // a 0 | b 0 | c 0 | d 0 + // + // + // + // + // ` + // ); + + // shift.click(); // first batch resolves, with it second can now resolve as well + // await tick(); + // assert.htmlEqual( + // target.innerHTML, + // ` + // a 1 | b 2 | c 1 | d 3 + // + // + // + // + // ` + // ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-7/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-7/main.svelte new file mode 100644 index 0000000000..cf028718c2 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-7/main.svelte @@ -0,0 +1,23 @@ + + +a {a} | b {b} | c {c} | d {d} + + + + \ No newline at end of file diff --git a/packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-fork-1/_config.js b/packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-fork-1/_config.js new file mode 100644 index 0000000000..07ce7e340f --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-fork-1/_config.js @@ -0,0 +1,34 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + await tick(); + const [a_b_fork, a_c, shift, pop, commit] = target.querySelectorAll('button'); + const [p] = target.querySelectorAll('p'); + + a_b_fork.click(); + await tick(); + assert.htmlEqual(p.innerHTML, 'a 0 | b 0 | c 0'); + + a_c.click(); + await tick(); + assert.htmlEqual(p.innerHTML, 'a 0 | b 0 | c 0'); + + pop.click(); + await tick(); + assert.htmlEqual(p.innerHTML, 'a 1 | b 0 | c 1'); + + shift.click(); + await tick(); + assert.htmlEqual(p.innerHTML, 'a 1 | b 0 | c 1'); + + shift.click(); + await tick(); + assert.htmlEqual(p.innerHTML, 'a 1 | b 0 | c 1'); + + commit.click(); + await tick(); + assert.htmlEqual(p.innerHTML, 'a 1 | b 1 | c 1'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-fork-1/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-fork-1/main.svelte new file mode 100644 index 0000000000..60ba74fb8e --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-fork-1/main.svelte @@ -0,0 +1,27 @@ + + +

a {await delay(a)} | b {await delay(b)} | c {c}

+ + + + + + diff --git a/packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-fork-2/_config.js b/packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-fork-2/_config.js new file mode 100644 index 0000000000..180a190dae --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-fork-2/_config.js @@ -0,0 +1,42 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + await tick(); + const [a_b_fork, a_c, b_d, shift, pop, commit] = target.querySelectorAll('button'); + const [p] = target.querySelectorAll('p'); + + a_b_fork.click(); + await tick(); + assert.htmlEqual(p.innerHTML, 'a 0 | b 0 | c 0 | d 0'); + + a_c.click(); + await tick(); + assert.htmlEqual(p.innerHTML, 'a 0 | b 0 | c 0 | d 0'); + + b_d.click(); + await tick(); + assert.htmlEqual(p.innerHTML, 'a 0 | b 0 | c 0 | d 0'); + + pop.click(); + await tick(); + assert.htmlEqual(p.innerHTML, 'a 0 | b 1 | c 0 | d 1'); + + pop.click(); + await tick(); + assert.htmlEqual(p.innerHTML, 'a 1 | b 1 | c 1 | d 1'); + + shift.click(); + await tick(); + assert.htmlEqual(p.innerHTML, 'a 1 | b 1 | c 1 | d 1'); + + shift.click(); + await tick(); + assert.htmlEqual(p.innerHTML, 'a 1 | b 1 | c 1 | d 1'); + + commit.click(); + await tick(); + assert.htmlEqual(p.innerHTML, 'a 1 | b 1 | c 1 | d 1'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-fork-2/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-fork-2/main.svelte new file mode 100644 index 0000000000..6d1c4ab418 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-fork-2/main.svelte @@ -0,0 +1,31 @@ + + +

a {await delay(a)} | b {await delay(b)} | c {c} | d {d}

+ + + + + + + diff --git a/packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-fork-3/_config.js b/packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-fork-3/_config.js new file mode 100644 index 0000000000..61360d8dc9 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-fork-3/_config.js @@ -0,0 +1,43 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + skip: true, + async test({ assert, target }) { + await tick(); + const [a_b_fork, a_c, b_d, shift, pop, commit] = target.querySelectorAll('button'); + const [p] = target.querySelectorAll('p'); + + a_b_fork.click(); + await tick(); + assert.htmlEqual(p.innerHTML, 'a 0 | b 0 | c 0 | d 0'); + + a_c.click(); + await tick(); + assert.htmlEqual(p.innerHTML, 'a 0 | b 0 | c 0 | d 0'); + + b_d.click(); + await tick(); + assert.htmlEqual(p.innerHTML, 'a 0 | b 0 | c 0 | d 0'); + + shift.click(); + await tick(); + assert.htmlEqual(p.innerHTML, 'a 0 | b 0 | c 0 | d 0'); + + shift.click(); + await tick(); + assert.htmlEqual(p.innerHTML, 'a 0 | b 0 | c 0 | d 0'); + + shift.click(); + await tick(); + assert.htmlEqual(p.innerHTML, 'a 1 | b 0 | c 1 | d 0'); + + shift.click(); + await tick(); + assert.htmlEqual(p.innerHTML, 'a 1 | b 1 | c 1 | d 1'); + + commit.click(); + await tick(); + assert.htmlEqual(p.innerHTML, 'a 1 | b 1 | c 1 | d 1'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-fork-3/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-fork-3/main.svelte new file mode 100644 index 0000000000..6d1c4ab418 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-fork-3/main.svelte @@ -0,0 +1,31 @@ + + +

a {await delay(a)} | b {await delay(b)} | c {c} | d {d}

+ + + + + + + diff --git a/packages/svelte/tests/runtime-runes/samples/async-overlapping-array/_config.js b/packages/svelte/tests/runtime-runes/samples/async-overlapping-array/_config.js new file mode 100644 index 0000000000..2dad5babea --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-overlapping-array/_config.js @@ -0,0 +1,205 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + await tick(); + + const [add, shift, pop] = target.querySelectorAll('button'); + + add.click(); + await tick(); + add.click(); + await tick(); + add.click(); + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` + + + +

pending=6 values.length=1 values=[1]

+
not keyed: +
1
+
+
keyed: +
1
+
+ ` + ); + + shift.click(); + await tick(); + shift.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + +

pending=4 values.length=2 values=[1,2]

+
not keyed: +
1
+
2
+
+
keyed: +
1
+
2
+
+ ` + ); + + shift.click(); + await tick(); + shift.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + +

pending=2 values.length=3 values=[1,2,3]

+
not keyed: +
1
+
2
+
3
+
+
keyed: +
1
+
2
+
3
+
+ ` + ); + + shift.click(); + await tick(); + shift.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + +

pending=0 values.length=4 values=[1,2,3,4]

+
not keyed: +
1
+
2
+
3
+
4
+
+
keyed: +
1
+
2
+
3
+
4
+
+ ` + ); + + add.click(); + await tick(); + add.click(); + await tick(); + add.click(); + await tick(); + add.click(); + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` + + + +

pending=8 values.length=4 values=[1,2,3,4]

+
not keyed: +
1
+
2
+
3
+
4
+
+
keyed: +
1
+
2
+
3
+
4
+
+ ` + ); + + // pop should have no effect until earlier promises have also resolved + pop.click(); + await tick(); + pop.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + +

pending=6 values.length=4 values=[1,2,3,4]

+
not keyed: +
1
+
2
+
3
+
4
+
+
keyed: +
1
+
2
+
3
+
4
+
+ ` + ); + + pop.click(); + await tick(); + pop.click(); + await tick(); + pop.click(); + await tick(); + pop.click(); + await tick(); + pop.click(); + await tick(); + pop.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + +

pending=0 values.length=8 values=[1,2,3,4,5,6,7,8]

+
not keyed: +
1
+
2
+
3
+
4
+
5
+
6
+
7
+
8
+
+
keyed: +
1
+
2
+
3
+
4
+
5
+
6
+
7
+
8
+
+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-each-overlap/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-overlapping-array/main.svelte similarity index 84% rename from packages/svelte/tests/runtime-runes/samples/async-each-overlap/main.svelte rename to packages/svelte/tests/runtime-runes/samples/async-overlapping-array/main.svelte index af9d395457..e41b6926b8 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-each-overlap/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/async-overlapping-array/main.svelte @@ -11,18 +11,14 @@ return p.promise; } - function shift() { - const fn = queue.shift(); - if (fn) fn(); - } - function addValue() { values.push(values.length+1); } - + +

pending={$effect.pending()} diff --git a/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss-for-await-break-return/_config.js b/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss-for-await-break-return/_config.js new file mode 100644 index 0000000000..3429380800 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss-for-await-break-return/_config.js @@ -0,0 +1,21 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; +import { normalise_trace_logs } from '../../../helpers.js'; + +export default test({ + compileOptions: { + dev: true + }, + html: '

pending

', + async test({ assert, target, warnings }) { + await tick(); + + assert.htmlEqual(target.innerHTML, '

number -> number -> number -> return -> ended

'); + + assert.deepEqual(normalise_trace_logs(warnings), [ + { + log: 'Detected reactivity loss when reading `values.length`. This happens when state is read in an async function after an earlier `await`' + } + ]); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss-for-await-break-return/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss-for-await-break-return/main.svelte new file mode 100644 index 0000000000..05ad7a6ee5 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss-for-await-break-return/main.svelte @@ -0,0 +1,41 @@ + + + +

{await get_result()}

+ + {#snippet pending()} +

pending

+ {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-1/Child.svelte b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-1/Child.svelte new file mode 100644 index 0000000000..6b765526c8 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-1/Child.svelte @@ -0,0 +1,7 @@ + + +{x} diff --git a/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-1/_config.js b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-1/_config.js new file mode 100644 index 0000000000..dee8af2446 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-1/_config.js @@ -0,0 +1,45 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target, logs }) { + const [x, y, resolve] = target.querySelectorAll('button'); + + x.click(); + await tick(); + + y.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + + world + ` // if this does not show world - that would also be ok + ); + + resolve.click(); + await tick(); + 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( + target.innerHTML, + ` + + + + universe + universe + universe + ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-1/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-1/main.svelte new file mode 100644 index 0000000000..8edc718de2 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-1/main.svelte @@ -0,0 +1,28 @@ + + + + + + + + +{#if x === 'universe'} + {await delay(x)} + +{/if} + +{#if y > 0} + +{/if} diff --git a/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-2/Child.svelte b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-2/Child.svelte new file mode 100644 index 0000000000..f8c01e9efd --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-2/Child.svelte @@ -0,0 +1,11 @@ + + + +{x} +{JSON.stringify(x)} +{#if x === 'universe'}universe{:else}world{/if} +{#if JSON.stringify(x) === '"universe"'}universe{:else}world{/if} +{await Promise.resolve(x)} +{await Promise.resolve(JSON.stringify(x))} \ No newline at end of file diff --git a/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-2/_config.js b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-2/_config.js new file mode 100644 index 0000000000..d99f0df731 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-2/_config.js @@ -0,0 +1,54 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + const [x, y, resolve] = target.querySelectorAll('button'); + + x.click(); + await tick(); + + y.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + +
+ 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(); + assert.htmlEqual( + target.innerHTML, + ` + + + + universe + universe + "universe" + universe + universe + universe + "universe" +
+ universe + "universe" + universe + universe + universe + "universe" + ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-2/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-2/main.svelte new file mode 100644 index 0000000000..11c4bdf5d1 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-2/main.svelte @@ -0,0 +1,30 @@ + + + + + + + + +{#if x === 'universe'} + {await delay(x)} + +{/if} + +
+ +{#if y > 0} + +{/if} diff --git a/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-3/Child.svelte b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-3/Child.svelte new file mode 100644 index 0000000000..f8c01e9efd --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-3/Child.svelte @@ -0,0 +1,11 @@ + + + +{x} +{JSON.stringify(x)} +{#if x === 'universe'}universe{:else}world{/if} +{#if JSON.stringify(x) === '"universe"'}universe{:else}world{/if} +{await Promise.resolve(x)} +{await Promise.resolve(JSON.stringify(x))} \ No newline at end of file diff --git a/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-3/_config.js b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-3/_config.js new file mode 100644 index 0000000000..eb4485e8a6 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-3/_config.js @@ -0,0 +1,68 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + const [x, y, resolve] = target.querySelectorAll('button'); + + x.click(); + await tick(); + + y.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + +
+ ` + ); + + resolve.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + +
+ 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(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + + universe + universe + "universe" + universe + universe + universe + "universe" +
+ universe + "universe" + universe + universe + universe + "universe" + ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-3/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-3/main.svelte new file mode 100644 index 0000000000..c8a4ca587f --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-3/main.svelte @@ -0,0 +1,33 @@ + + + + + + + +{#if x === 'universe'} + {await delay(x)} + +{/if} + +
+ +{#if y > 0} + +{/if} diff --git a/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-all-combinations/Child.svelte b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-all-combinations/Child.svelte new file mode 100644 index 0000000000..f8c01e9efd --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-all-combinations/Child.svelte @@ -0,0 +1,11 @@ + + + +{x} +{JSON.stringify(x)} +{#if x === 'universe'}universe{:else}world{/if} +{#if JSON.stringify(x) === '"universe"'}universe{:else}world{/if} +{await Promise.resolve(x)} +{await Promise.resolve(JSON.stringify(x))} \ No newline at end of file diff --git a/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-all-combinations/_config.js b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-all-combinations/_config.js new file mode 100644 index 0000000000..8b7e7784de --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-all-combinations/_config.js @@ -0,0 +1,171 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + skip: true, // TODO more combinations pass on https://github.com/sveltejs/svelte/pull/17971 + timeout: 20_000, + async test({ assert, target }) { + const [x, fork_x, y, fork_y, shift, pop, commit_x, commit_y, reset] = + target.querySelectorAll('button'); + + const initial = ` + + + + + + + + + +
+ `; + + const final = ` + + + + + + + + + + universe + universe + "universe" + universe + universe + universe + "universe" +
+ universe + "universe" + universe + universe + universe + "universe" + `; + + /** @param {HTMLElement} button */ + async function click(button) { + button.click(); + await tick(); + } + + /** + * Generate all permutations of an array. + * @param {HTMLElement[]} actions + * @returns {HTMLElement[][]} + */ + function permutations(actions) { + if (actions.length <= 1) return [actions]; + + /** @type {HTMLElement[][]} */ + const result = []; + + for (let i = 0; i < actions.length; i++) { + const head = actions[i]; + const rest = actions.slice(0, i).concat(actions.slice(i + 1)); + for (const tail of permutations(rest)) { + result.push([head, ...tail]); + } + } + + return result; + } + + /** + * Keep only valid orders where fork commits happen after their fork action. + * @param {HTMLElement[]} order + */ + function is_valid_order(order) { + const x_fork_index = order.indexOf(fork_x); + const commit_x_index = order.indexOf(commit_x); + if (commit_x_index !== -1 && (x_fork_index === -1 || commit_x_index < x_fork_index)) { + return false; + } + + const y_fork_index = order.indexOf(fork_y); + const commit_y_index = order.indexOf(commit_y); + if (commit_y_index !== -1 && (y_fork_index === -1 || commit_y_index < y_fork_index)) { + return false; + } + + return true; + } + + /** + * Four control scenarios: + * - x direct, y direct + * - x direct, y via fork+commit + * - x via fork+commit, y direct + * - x via fork+commit, y via fork+commit + */ + const control_scenarios = [ + [x, y], + [x, fork_y, commit_y], + [fork_x, commit_x, y], + [fork_x, commit_x, fork_y, commit_y] + ]; + + const control_orders = control_scenarios.flatMap((scenario) => + permutations(scenario).filter(is_valid_order) + ); + + /** + * All shift/pop combinations for draining async work. + * We click three times because this scenario can queue up to 3 deferred resolutions. + */ + const resolve_orders = [ + [shift, shift, shift], + [shift, pop, pop], + [pop, shift, shift], + [pop, pop, pop] + ]; + + for (const controls of control_orders) { + for (const resolves of resolve_orders) { + for (const action of controls) { + await click(action); + } + + for (const action of resolves) { + await click(action); + } + + const failure_msg = `Failed for: ${controls + .map((btn) => btn.textContent) + .concat(...resolves.map((btn) => btn.textContent)) + .join(', ')}`; + assert.htmlEqual(target.innerHTML, final, failure_msg); + + await click(reset); + assert.htmlEqual(target.innerHTML, initial, failure_msg); + } + } + + const other_scenarios = [ + [x, shift, y, shift, shift], + [x, shift, y, pop, pop], + [fork_x, shift, y, shift, commit_x, shift], + [fork_x, shift, y, pop, commit_x, pop], + [y, shift, x, shift, shift], + [y, shift, x, pop, pop], + [fork_y, shift, x, shift, commit_y, shift], + [fork_y, shift, x, pop, commit_y, pop] + ]; + + for (const scenario of other_scenarios) { + for (const action of scenario) { + await click(action); + } + + const failure_msg = `Failed for: ${scenario.map((btn) => btn.textContent).join(', ')}`; + assert.htmlEqual(target.innerHTML, final, failure_msg); + + await click(reset); + assert.htmlEqual(target.innerHTML, initial, failure_msg); + } + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-all-combinations/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-all-combinations/main.svelte new file mode 100644 index 0000000000..bb821d9b0f --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-all-combinations/main.svelte @@ -0,0 +1,41 @@ + + + + + + + + + + + + +{#if x === 'universe'} + {await delay(x)} + +{/if} + +
+ +{#if y > 0} + +{/if} diff --git a/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-fork-1/Child.svelte b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-fork-1/Child.svelte new file mode 100644 index 0000000000..f8c01e9efd --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-fork-1/Child.svelte @@ -0,0 +1,11 @@ + + + +{x} +{JSON.stringify(x)} +{#if x === 'universe'}universe{:else}world{/if} +{#if JSON.stringify(x) === '"universe"'}universe{:else}world{/if} +{await Promise.resolve(x)} +{await Promise.resolve(JSON.stringify(x))} \ No newline at end of file diff --git a/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-fork-1/_config.js b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-fork-1/_config.js new file mode 100644 index 0000000000..737169cb91 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-fork-1/_config.js @@ -0,0 +1,90 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + skip: true, // TODO works on https://github.com/sveltejs/svelte/pull/17971 + async test({ assert, target }) { + const [x, y, shift, pop, commit] = target.querySelectorAll('button'); + + x.click(); + await tick(); + + y.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + + + +
+ ` + ); + + commit.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + + + +
+ ` + ); + + shift.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + + + + universe + universe + "universe" + universe + universe + universe + "universe" +
+ ` + ); + + shift.click(); + await tick(); + shift.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + + + + universe + universe + "universe" + universe + universe + universe + "universe" +
+ universe + "universe" + universe + universe + universe + "universe" + ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-fork-1/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-fork-1/main.svelte new file mode 100644 index 0000000000..5f8d5efa13 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-fork-1/main.svelte @@ -0,0 +1,36 @@ + + + + + + + + +{#if x === 'universe'} + {await delay(x)} + +{/if} + +
+ +{#if y > 0} + +{/if} diff --git a/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-fork-2/Child.svelte b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-fork-2/Child.svelte new file mode 100644 index 0000000000..f8c01e9efd --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-fork-2/Child.svelte @@ -0,0 +1,11 @@ + + + +{x} +{JSON.stringify(x)} +{#if x === 'universe'}universe{:else}world{/if} +{#if JSON.stringify(x) === '"universe"'}universe{:else}world{/if} +{await Promise.resolve(x)} +{await Promise.resolve(JSON.stringify(x))} \ No newline at end of file diff --git a/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-fork-2/_config.js b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-fork-2/_config.js new file mode 100644 index 0000000000..74df968d82 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-fork-2/_config.js @@ -0,0 +1,72 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + const [x, y, shift, pop, commit] = target.querySelectorAll('button'); + + y.click(); + await tick(); + + x.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + + + +
+ ` + ); + + commit.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + + + +
+ ` + ); + + shift.click(); + await tick(); + shift.click(); + await tick(); + shift.click(); + await tick(); + shift.click(); // would be ok to not need this one + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + + + + universe + universe + "universe" + universe + universe + universe + "universe" +
+ universe + "universe" + universe + universe + universe + "universe" + ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-fork-2/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-fork-2/main.svelte new file mode 100644 index 0000000000..5f8d5efa13 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-fork-2/main.svelte @@ -0,0 +1,36 @@ + + + + + + + + +{#if x === 'universe'} + {await delay(x)} + +{/if} + +
+ +{#if y > 0} + +{/if} diff --git a/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-fork-3/Child.svelte b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-fork-3/Child.svelte new file mode 100644 index 0000000000..f8c01e9efd --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-fork-3/Child.svelte @@ -0,0 +1,11 @@ + + + +{x} +{JSON.stringify(x)} +{#if x === 'universe'}universe{:else}world{/if} +{#if JSON.stringify(x) === '"universe"'}universe{:else}world{/if} +{await Promise.resolve(x)} +{await Promise.resolve(JSON.stringify(x))} \ No newline at end of file diff --git a/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-fork-3/_config.js b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-fork-3/_config.js new file mode 100644 index 0000000000..905e76511b --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-fork-3/_config.js @@ -0,0 +1,70 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + const [x, y, shift, pop, commit] = target.querySelectorAll('button'); + + y.click(); + await tick(); + + x.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + + + +
+ ` + ); + + commit.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + + + +
+ ` + ); + + pop.click(); + await tick(); + pop.click(); + await tick(); + pop.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + + + + universe + universe + "universe" + universe + universe + universe + "universe" +
+ universe + "universe" + universe + universe + universe + "universe" + ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-fork-3/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-fork-3/main.svelte new file mode 100644 index 0000000000..5f8d5efa13 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-fork-3/main.svelte @@ -0,0 +1,36 @@ + + + + + + + + +{#if x === 'universe'} + {await delay(x)} + +{/if} + +
+ +{#if y > 0} + +{/if} diff --git a/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-fork-4/Child.svelte b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-fork-4/Child.svelte new file mode 100644 index 0000000000..f8c01e9efd --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-fork-4/Child.svelte @@ -0,0 +1,11 @@ + + + +{x} +{JSON.stringify(x)} +{#if x === 'universe'}universe{:else}world{/if} +{#if JSON.stringify(x) === '"universe"'}universe{:else}world{/if} +{await Promise.resolve(x)} +{await Promise.resolve(JSON.stringify(x))} \ No newline at end of file diff --git a/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-fork-4/_config.js b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-fork-4/_config.js new file mode 100644 index 0000000000..6a6399a19e --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-fork-4/_config.js @@ -0,0 +1,76 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + skip: true, // TODO works on https://github.com/sveltejs/svelte/pull/17971 + async test({ assert, target }) { + const [x, y, resolve, commit] = target.querySelectorAll('button'); + + x.click(); + await tick(); + + y.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + + +
+ world + "world" + world + world + world + "world" + ` + ); + + commit.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + + +
+ world + "world" + world + world + world + "world" + ` + ); + + resolve.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + + + universe + universe + "universe" + universe + universe + universe + "universe" +
+ universe + "universe" + universe + universe + universe + "universe" + ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-fork-4/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-fork-4/main.svelte new file mode 100644 index 0000000000..5f00fc4dec --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-fork-4/main.svelte @@ -0,0 +1,32 @@ + + + + + + + + +{#if x === 'universe'} + {await delay(x)} + +{/if} + +
+ +{#if y > 0} + +{/if} diff --git a/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-fork-5/Child.svelte b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-fork-5/Child.svelte new file mode 100644 index 0000000000..f8c01e9efd --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-fork-5/Child.svelte @@ -0,0 +1,11 @@ + + + +{x} +{JSON.stringify(x)} +{#if x === 'universe'}universe{:else}world{/if} +{#if JSON.stringify(x) === '"universe"'}universe{:else}world{/if} +{await Promise.resolve(x)} +{await Promise.resolve(JSON.stringify(x))} \ No newline at end of file diff --git a/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-fork-5/_config.js b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-fork-5/_config.js new file mode 100644 index 0000000000..e8f16ade3c --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-fork-5/_config.js @@ -0,0 +1,84 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + const [x, y, resolve, commit] = target.querySelectorAll('button'); + + x.click(); + await tick(); + + y.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + + +
+ ` + ); + + commit.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + + +
+ ` + ); + + resolve.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + + +
+ world + "world" + world + world + world + "world" + ` + ); + + resolve.click(); + await tick(); + resolve.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + + + universe + universe + "universe" + universe + universe + universe + "universe" +
+ universe + "universe" + universe + universe + universe + "universe" + ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-fork-5/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-fork-5/main.svelte new file mode 100644 index 0000000000..5575e3cbd4 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-fork-5/main.svelte @@ -0,0 +1,36 @@ + + + + + + + + +{#if x === 'universe'} + {await delay(x)} + +{/if} + +
+ +{#if y > 0} + +{/if} diff --git a/packages/svelte/tests/runtime-runes/samples/async-state-new-branch/_config.js b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch/_config.js index f2091eb6ab..f4b6cc777b 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-state-new-branch/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch/_config.js @@ -2,7 +2,6 @@ import { tick } from 'svelte'; import { test } from '../../test'; export default test({ - skip: true, // this fails on main, too; skip for now async test({ assert, target, logs }) { const [x, y, resolve] = target.querySelectorAll('button'); diff --git a/packages/svelte/tests/runtime-runes/samples/await-html-hydration/_config.js b/packages/svelte/tests/runtime-runes/samples/await-html-hydration/_config.js index e7983a3de9..87a697b18f 100644 --- a/packages/svelte/tests/runtime-runes/samples/await-html-hydration/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/await-html-hydration/_config.js @@ -3,5 +3,7 @@ import { test } from '../../test'; export default test({ skip_no_async: true, mode: ['hydrate'], - async test() {} + async test({ assert, warnings }) { + assert.deepEqual(warnings, []); // TODO not quite sure why this isn't populated yet + } }); diff --git a/packages/svelte/tests/runtime-runes/samples/typescript/main.svelte b/packages/svelte/tests/runtime-runes/samples/typescript/main.svelte index 4fc7c4ec38..0286f2c0fd 100644 --- a/packages/svelte/tests/runtime-runes/samples/typescript/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/typescript/main.svelte @@ -27,6 +27,11 @@ abstract x(): void; y() {} } + class Subclass extends Foo { + constructor(value: string) { + super(value); + } + } declare const declared_const: number; declare function declared_fn(): void; diff --git a/packages/svelte/tests/snapshot/samples/async-in-derived/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/async-in-derived/_expected/client/index.svelte.js index a8b1a43495..4f06d9ddbf 100644 --- a/packages/svelte/tests/snapshot/samples/async-in-derived/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/async-in-derived/_expected/client/index.svelte.js @@ -10,13 +10,15 @@ export default function Async_in_derived($$anchor, $$props) { var $$promises = $.run([ async () => yes1 = await $.async_derived(() => 1), async () => yes2 = await $.async_derived(async () => foo(await 1)), - () => no1 = $.derived(async () => { - return await 1; - }), - - () => no2 = $.derived(() => async () => { - return await 1; - }) + () => { + no1 = $.derived(async () => { + return await 1; + }); + + no2 = $.derived(() => async () => { + return await 1; + }); + } ]); var fragment = $.comment(); diff --git a/packages/svelte/tests/snapshot/samples/async-in-derived/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/async-in-derived/_expected/server/index.svelte.js index 1697e3adc6..3a53475944 100644 --- a/packages/svelte/tests/snapshot/samples/async-in-derived/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/async-in-derived/_expected/server/index.svelte.js @@ -8,13 +8,15 @@ export default function Async_in_derived($$renderer, $$props) { var $$promises = $$renderer.run([ async () => yes1 = await $.async_derived(() => 1), async () => yes2 = await $.async_derived(async () => foo(await 1)), - () => no1 = $.derived(async () => { - return await 1; - }), - - () => no2 = $.derived(() => async () => { - return await 1; - }) + () => { + no1 = $.derived(async () => { + return await 1; + }); + + no2 = $.derived(() => async () => { + return await 1; + }); + } ]); if (true) { diff --git a/packages/svelte/tests/snapshot/samples/async-top-level-group-sync-run/_config.js b/packages/svelte/tests/snapshot/samples/async-top-level-group-sync-run/_config.js new file mode 100644 index 0000000000..2e30bbeb16 --- /dev/null +++ b/packages/svelte/tests/snapshot/samples/async-top-level-group-sync-run/_config.js @@ -0,0 +1,3 @@ +import { test } from '../../test'; + +export default test({ compileOptions: { experimental: { async: true } } }); diff --git a/packages/svelte/tests/snapshot/samples/async-top-level-group-sync-run/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/async-top-level-group-sync-run/_expected/client/index.svelte.js new file mode 100644 index 0000000000..8fb09fadd2 --- /dev/null +++ b/packages/svelte/tests/snapshot/samples/async-top-level-group-sync-run/_expected/client/index.svelte.js @@ -0,0 +1,26 @@ +import 'svelte/internal/disclose-version'; +import 'svelte/internal/flags/async'; +import * as $ from 'svelte/internal/client'; + +export default function Async_top_level_group_sync_run($$anchor) { + var a, + // these should be grouped into one, having an async tick inbetween + // would change how the code runs and could introduce subtle timing bugs + b, + c; + + var $$promises = $.run([ + async () => a = await Promise.resolve(1), + () => { + b = a + 1; + c = b + 1; + } + ]); + + $.next(); + + var text = $.text(); + + $.template_effect(() => $.set_text(text, c), void 0, void 0, [$$promises[1]]); + $.append($$anchor, text); +} \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/async-top-level-group-sync-run/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/async-top-level-group-sync-run/_expected/server/index.svelte.js new file mode 100644 index 0000000000..43db129746 --- /dev/null +++ b/packages/svelte/tests/snapshot/samples/async-top-level-group-sync-run/_expected/server/index.svelte.js @@ -0,0 +1,21 @@ +import 'svelte/internal/flags/async'; +import * as $ from 'svelte/internal/server'; + +export default function Async_top_level_group_sync_run($$renderer) { + var a, + // these should be grouped into one, having an async tick inbetween + // would change how the code runs and could introduce subtle timing bugs + b, + c; + + var $$promises = $$renderer.run([ + async () => a = await Promise.resolve(1), + () => { + b = a + 1; + c = b + 1; + } + ]); + + $$renderer.push(``); + $$renderer.async([$$promises[1]], ($$renderer) => $$renderer.push(() => $.escape(c))); +} \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/async-top-level-group-sync-run/index.svelte b/packages/svelte/tests/snapshot/samples/async-top-level-group-sync-run/index.svelte new file mode 100644 index 0000000000..98f602312e --- /dev/null +++ b/packages/svelte/tests/snapshot/samples/async-top-level-group-sync-run/index.svelte @@ -0,0 +1,9 @@ + + +{c} diff --git a/packages/svelte/tests/suite.ts b/packages/svelte/tests/suite.ts index bbd252b8e1..af36f16e29 100644 --- a/packages/svelte/tests/suite.ts +++ b/packages/svelte/tests/suite.ts @@ -4,6 +4,7 @@ import { it } from 'vitest'; export interface BaseTest { skip?: boolean; solo?: boolean; + timeout?: number; } /** @@ -30,7 +31,7 @@ export function suite(fn: (config: Test, test_dir: string await for_each_dir(cwd, samples_dir, (config, dir) => { let it_fn = config.skip ? it.skip : config.solo ? it.only : it; - it_fn(dir, () => fn(config, `${cwd}/${samples_dir}/${dir}`)); + it_fn(dir, { timeout: config.timeout }, () => fn(config, `${cwd}/${samples_dir}/${dir}`)); }); } }; @@ -57,7 +58,7 @@ export function suite_with_variants { + it_fn(`${dir} (${variant})`, { timeout: config.timeout }, async () => { if (!called_common) { called_common = true; common = await common_setup(config, `${cwd}/${samples_dir}/${dir}`); diff --git a/packages/svelte/tests/types/motion.ts b/packages/svelte/tests/types/motion.ts new file mode 100644 index 0000000000..0ba5d1d9c6 --- /dev/null +++ b/packages/svelte/tests/types/motion.ts @@ -0,0 +1,22 @@ +import { + type TweenOptions, + type SpringOptions, + type SpringUpdateOptions, + type Updater +} from 'svelte/motion'; + +let tweenOptions: TweenOptions = { + delay: 100, + duration: 400 +}; + +let springOptions: SpringOptions = { + stiffness: 0.1, + damping: 0.5 +}; + +let springUpdateOptions: SpringUpdateOptions = { + instant: true +}; + +let updater: Updater = (target, value) => target + value; diff --git a/packages/svelte/tests/validator/samples/illegal-variable-declaration/errors.json b/packages/svelte/tests/validator/samples/illegal-variable-declaration/errors.json index 850880e744..cf413c39df 100644 --- a/packages/svelte/tests/validator/samples/illegal-variable-declaration/errors.json +++ b/packages/svelte/tests/validator/samples/illegal-variable-declaration/errors.json @@ -1,7 +1,7 @@ [ { "code": "declaration_duplicate_module_import", - "message": "Cannot declare a variable with the same name as an import inside ` * ``` */ - static of(fn: () => U, options?: SpringOpts): Spring; + static of(fn: () => U, options?: SpringOptions): Spring; /** * Sets `spring.target` to `value` and returns a `Promise` that resolves if and when `spring.current` catches up to it. @@ -2067,7 +2101,7 @@ declare module 'svelte/motion' { * If `options.preserveMomentum` is provided, the spring will continue on its current trajectory for * the specified number of milliseconds. This is useful for things like 'fling' gestures. */ - set(value: T, options?: SpringUpdateOpts): Promise; + set(value: T, options?: SpringUpdateOptions): Promise; damping: number; precision: number; @@ -2085,8 +2119,8 @@ declare module 'svelte/motion' { } export interface Tweened extends Readable { - set(value: T, opts?: TweenedOptions): Promise; - update(updater: Updater, opts?: TweenedOptions): Promise; + set(value: T, opts?: TweenOptions): Promise; + update(updater: Updater, opts?: TweenOptions): Promise; } /** Callback to inform of a value updates. */ type Subscriber = (value: T) => void; @@ -2103,39 +2137,6 @@ declare module 'svelte/motion' { */ subscribe(this: void, run: Subscriber, invalidate?: () => void): Unsubscriber; } - interface SpringOpts { - stiffness?: number; - damping?: number; - precision?: number; - } - - interface SpringUpdateOpts { - /** - * @deprecated Only use this for the spring store; does nothing when set on the Spring class - */ - hard?: any; - /** - * @deprecated Only use this for the spring store; does nothing when set on the Spring class - */ - soft?: string | number | boolean; - /** - * Only use this for the Spring class; does nothing when set on the spring store - */ - instant?: boolean; - /** - * Only use this for the Spring class; does nothing when set on the spring store - */ - preserveMomentum?: number; - } - - type Updater = (target_value: T, value: T) => T; - - interface TweenedOptions { - delay?: number; - duration?: number | ((from: T, to: T) => number); - easing?: (t: number) => number; - interpolate?: (a: T, b: T) => (t: number) => T; - } /** * A [media query](https://svelte.dev/docs/svelte/svelte-reactivity#MediaQuery) that matches if the user [prefers reduced motion](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-reduced-motion). * @@ -2165,13 +2166,13 @@ declare module 'svelte/motion' { * * @deprecated Use [`Spring`](https://svelte.dev/docs/svelte/svelte-motion#Spring) instead * */ - export function spring(value?: T | undefined, opts?: SpringOpts | undefined): Spring; + export function spring(value?: T | undefined, opts?: SpringOptions | undefined): Spring; /** * A tweened store in Svelte is a special type of store that provides smooth transitions between state values over time. * * @deprecated Use [`Tween`](https://svelte.dev/docs/svelte/svelte-motion#Tween) instead * */ - export function tweened(value?: T | undefined, defaults?: TweenedOptions | undefined): Tweened; + export function tweened(value?: T | undefined, defaults?: TweenOptions | undefined): Tweened; /** * A wrapper for a value that tweens smoothly to its target value. Changes to `tween.target` will cause `tween.current` to * move towards it over time, taking account of the `delay`, `duration` and `easing` options. @@ -2204,15 +2205,15 @@ declare module 'svelte/motion' { * ``` * */ - static of(fn: () => U, options?: TweenedOptions | undefined): Tween; + static of(fn: () => U, options?: TweenOptions | undefined): Tween; - constructor(value: T, options?: TweenedOptions); + constructor(value: T, options?: TweenOptions); /** * Sets `tween.target` to `value` and returns a `Promise` that resolves if and when `tween.current` catches up to it. * * If `options` are provided, they will override the tween's defaults. * */ - set(value: T, options?: TweenedOptions | undefined): Promise; + set(value: T, options?: TweenOptions | undefined): Promise; get current(): T; set target(v: T); get target(): T; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index aa423e4923..2987749c2a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -102,8 +102,8 @@ importers: specifier: ^1.2.1 version: 1.2.1 esrap: - specifier: ^2.2.2 - version: 2.2.2 + specifier: ^2.2.4 + version: 2.2.4 is-reference: specifier: ^3.0.3 version: 3.0.3 @@ -1418,8 +1418,8 @@ packages: resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} engines: {node: '>=0.10'} - esrap@2.2.2: - resolution: {integrity: sha512-zA6497ha+qKvoWIK+WM9NAh5ni17sKZKhbS5B3PoYbBvaYHZWoS33zmFybmyqpn07RLUxSmn+RCls2/XF+d0oQ==} + esrap@2.2.4: + resolution: {integrity: sha512-suICpxAmZ9A8bzJjEl/+rLJiDKC0X4gYWUxT6URAWBLvlXmtbZd5ySMu/N2ZGEtMCAmflUDPSehrP9BQcsGcSg==} esrecurse@4.3.0: resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} @@ -3794,9 +3794,10 @@ snapshots: dependencies: estraverse: 5.3.0 - esrap@2.2.2: + esrap@2.2.4: dependencies: '@jridgewell/sourcemap-codec': 1.5.0 + '@typescript-eslint/types': 8.56.0 esrecurse@4.3.0: dependencies: