diff --git a/.changeset/orange-wasps-visit.md b/.changeset/orange-wasps-visit.md new file mode 100644 index 0000000000..972ff636b8 --- /dev/null +++ b/.changeset/orange-wasps-visit.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: don't swallow `DOMException` when `media.play()` fails in `bind:paused` diff --git a/.changeset/smooth-comics-shine.md b/.changeset/smooth-comics-shine.md deleted file mode 100644 index 4bf26ed9f1..0000000000 --- a/.changeset/smooth-comics-shine.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: correctly print `!doctype` during `print` diff --git a/.changeset/wild-dolls-hang.md b/.changeset/wild-dolls-hang.md new file mode 100644 index 0000000000..a7b3436d69 --- /dev/null +++ b/.changeset/wild-dolls-hang.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: reduce if block nesting diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 51408fc8cc..23d814d527 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -32,9 +32,9 @@ jobs: os: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - uses: pnpm/action-setup@v4 - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v6 with: node-version: ${{ matrix.node-version }} cache: pnpm @@ -48,9 +48,9 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 10 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - uses: pnpm/action-setup@v4 - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v6 with: node-version: 22 cache: pnpm @@ -65,9 +65,9 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 5 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - uses: pnpm/action-setup@v4 - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v6 with: node-version: 24 cache: pnpm @@ -82,11 +82,11 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 5 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - uses: pnpm/action-setup@v4 - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v6 with: - node-version: 18 + node-version: 24 cache: pnpm - name: install run: pnpm install --frozen-lockfile @@ -103,9 +103,9 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 15 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - uses: pnpm/action-setup@v4 - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v6 with: node-version: 18 cache: pnpm diff --git a/.github/workflows/pkg.pr.new-comment.yml b/.github/workflows/pkg.pr.new-comment.yml deleted file mode 100644 index 8bf13bb8f1..0000000000 --- a/.github/workflows/pkg.pr.new-comment.yml +++ /dev/null @@ -1,115 +0,0 @@ -name: Update pkg.pr.new comment - -on: - workflow_run: - workflows: ['Publish Any Commit'] - types: - - completed - -permissions: - pull-requests: write - -jobs: - build: - name: 'Update comment' - runs-on: ubuntu-latest - steps: - - name: Download artifact - uses: actions/download-artifact@v4 - with: - name: output - github-token: ${{ secrets.GITHUB_TOKEN }} - run-id: ${{ github.event.workflow_run.id }} - - - run: ls -R . - - name: 'Post or update comment' - uses: actions/github-script@v6 - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - const fs = require('fs'); - const output = JSON.parse(fs.readFileSync('output.json', 'utf8')); - - const bot_comment_identifier = ``; - - const body = (number) => `${bot_comment_identifier} - - [Playground](https://svelte.dev/playground?version=pr-${number}) - - \`\`\` - ${output.packages.map((p) => `pnpm add https://pkg.pr.new/${p.name}@${number}`).join('\n')} - \`\`\` - `; - - async function find_bot_comment(issue_number) { - if (!issue_number) return null; - const comments = await github.rest.issues.listComments({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: issue_number, - }); - return comments.data.find((comment) => - comment.body.includes(bot_comment_identifier) - ); - } - - async function create_or_update_comment(issue_number) { - if (!issue_number) { - console.log('No issue number provided. Cannot post or update comment.'); - return; - } - - const existing_comment = await find_bot_comment(issue_number); - if (existing_comment) { - await github.rest.issues.updateComment({ - owner: context.repo.owner, - repo: context.repo.repo, - comment_id: existing_comment.id, - body: body(issue_number), - }); - } else { - await github.rest.issues.createComment({ - issue_number: issue_number, - owner: context.repo.owner, - repo: context.repo.repo, - body: body(issue_number), - }); - } - } - - async function log_publish_info() { - const svelte_package = output.packages.find(p => p.name === 'svelte'); - const svelte_sha = svelte_package.url.replace(/^.+@([^@]+)$/, '$1'); - console.log('\n' + '='.repeat(50)); - console.log('Publish Information'); - console.log('='.repeat(50)); - console.log('\nPublished Packages:'); - console.log(output.packages.map((p) => `${p.name} - pnpm add https://pkg.pr.new/${p.name}@${p.url.replace(/^.+@([^@]+)$/, '$1')}`).join('\n')); - if(svelte_sha){ - console.log('\nPlayground URL:'); - console.log(`\nhttps://svelte.dev/playground?version=commit-${svelte_sha}`) - } - console.log('\n' + '='.repeat(50)); - } - - if (output.event_name === 'pull_request') { - if (output.number) { - await create_or_update_comment(output.number); - } - } else if (output.event_name === 'push') { - const pull_requests = await github.rest.pulls.list({ - owner: context.repo.owner, - repo: context.repo.repo, - state: 'open', - head: `${context.repo.owner}:${output.ref.replace('refs/heads/', '')}`, - }); - - if (pull_requests.data.length > 0) { - await create_or_update_comment(pull_requests.data[0].number); - } else { - console.log( - 'No open pull request found for this push. Logging publish information to console:' - ); - await log_publish_info(); - } - } diff --git a/.github/workflows/pkg.pr.new.yml b/.github/workflows/pkg.pr.new.yml index 49303f1684..0f0c433361 100644 --- a/.github/workflows/pkg.pr.new.yml +++ b/.github/workflows/pkg.pr.new.yml @@ -1,18 +1,53 @@ -name: Publish Any Commit -on: [push, pull_request] +name: pkg.pr.new +on: + pull_request_target: + types: [opened, synchronize] + push: + branches: [main] permissions: {} jobs: - build: - permissions: {} + # This job determines the environment to use for the build job. It ensures that: + # - For pushes to main, we use the "Publish pkg.pr.new (maintainers)" environment. + # - For PRs from the same repository, we also use the "Publish pkg.pr.new (maintainers)" environment, since these are trusted. + # - For PRs from forks, we use the "Publish pkg.pr.new (external contributors)" environment, which requires manual approval by a maintainer before the build job can run. + # This protects us from running untrusted code while still allowing external contributors to use pkg.pr.new. + resolve-env: + runs-on: ubuntu-latest + outputs: + environment: ${{ steps.resolve.outputs.environment }} + steps: + - name: Determine environment + id: resolve + run: | + if [[ "${{ github.event_name }}" == "push" ]]; then + echo "environment=Publish pkg.pr.new (maintainers)" >> "$GITHUB_OUTPUT" + elif [[ "${{ github.event.pull_request.head.repo.full_name }}" == "${{ github.repository }}" ]]; then + echo "environment=Publish pkg.pr.new (maintainers)" >> "$GITHUB_OUTPUT" + else + echo "environment=Publish pkg.pr.new (external contributors)" >> "$GITHUB_OUTPUT" + fi + build: + needs: resolve-env runs-on: ubuntu-latest + # This is the line that ensures forks require manual approval before running the build job + environment: ${{ needs.resolve-env.outputs.environment }} + + # No permissions — this job runs user-controlled code + permissions: {} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 + with: + # For pull_request_target, we must explicitly check out the PR head. + # This is safe because the environment gate above has already fired — + # an org member has approved this specific commit for external PRs. + ref: ${{ github.event.pull_request.head.sha || github.sha }} + - uses: pnpm/action-setup@v4 - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v6 with: node-version: 22.x cache: pnpm @@ -24,21 +59,161 @@ jobs: run: pnpm build - run: pnpx pkg-pr-new publish --comment=off --json output.json --compact --no-template './packages/svelte' - - name: Add metadata to output - uses: actions/github-script@v6 - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - const fs = require('fs'); - const output = JSON.parse(fs.readFileSync('output.json', 'utf8')); - output.number = context.issue.number; - output.event_name = context.eventName; - output.ref = context.ref; - fs.writeFileSync('output.json', JSON.stringify(output), 'utf8'); + - name: Upload output uses: actions/upload-artifact@v4 with: name: output path: ./output.json - - run: ls -R . + # Sanitizes the untrusted output from the build job before it's consumed by + # jobs with elevated permissions. This ensures that only known package names + # and valid SHA prefixes make it through. + sanitize: + needs: build + runs-on: ubuntu-latest + + permissions: {} + + steps: + - name: Download artifact + uses: actions/download-artifact@v7 + with: + name: output + + - name: Sanitize output + uses: actions/github-script@v8 + with: + script: | + const fs = require('fs'); + const raw = JSON.parse(fs.readFileSync('output.json', 'utf8')); + + const ALLOWED_PACKAGES = new Set(['svelte']); + const SHA_PATTERN = /^[0-9a-f]{7}$/; + + const packages = (raw.packages || []) + .filter(p => { + if (!ALLOWED_PACKAGES.has(p.name)) { + console.log(`Skipping unexpected package: ${JSON.stringify(p.name)}`); + return false; + } + const sha = p.url?.replace(/^.+@([^@]+)$/, '$1'); + if (!sha || !SHA_PATTERN.test(sha)) { + console.log(`Skipping package with invalid SHA: ${JSON.stringify(p.url)}`); + return false; + } + return true; + }) + .map(p => ({ + name: p.name, + sha: p.url.replace(/^.+@([^@]+)$/, '$1'), + })); + + fs.writeFileSync('sanitized-output.json', JSON.stringify({ packages }), 'utf8'); + + - name: Upload sanitized output + uses: actions/upload-artifact@v4 + with: + name: sanitized-output + path: ./sanitized-output.json + + comment: + needs: sanitize + if: github.event_name == 'pull_request_target' + runs-on: ubuntu-latest + + permissions: + pull-requests: write + + steps: + - name: Download sanitized artifact + uses: actions/download-artifact@v7 + with: + name: sanitized-output + + - name: Post or update comment + uses: actions/github-script@v8 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const fs = require('fs'); + const { packages } = JSON.parse(fs.readFileSync('sanitized-output.json', 'utf8')); + + if (packages.length === 0) { + console.log('No valid packages found. Skipping comment.'); + return; + } + + // Issue number from trusted event context, never from the artifact + const issue_number = context.issue.number; + + const bot_comment_identifier = ``; + + const body = `${bot_comment_identifier} + + [Playground](https://svelte.dev/playground?version=pr-${issue_number}) + + \`\`\` + ${packages.map(p => `pnpm add https://pkg.pr.new/${p.name}@${issue_number}`).join('\n')} + \`\`\` + `; + + const comments = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number, + }); + const existing = comments.data.find(c => c.body.includes(bot_comment_identifier)); + + if (existing) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: existing.id, + body, + }); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number, + body, + }); + } + + log: + needs: sanitize + if: github.event_name == 'push' + runs-on: ubuntu-latest + + permissions: {} + + steps: + - name: Download sanitized artifact + uses: actions/download-artifact@v7 + with: + name: sanitized-output + + - name: Log publish info + uses: actions/github-script@v8 + with: + script: | + const fs = require('fs'); + const { packages } = JSON.parse(fs.readFileSync('sanitized-output.json', 'utf8')); + + if (packages.length === 0) { + console.log('No valid packages found.'); + return; + } + + console.log('\n' + '='.repeat(50)); + console.log('Publish Information'); + console.log('='.repeat(50)); + for (const p of packages) { + console.log(`${p.name} - pnpm add https://pkg.pr.new/${p.name}@${p.sha}`); + } + const svelte = packages.find(p => p.name === 'svelte'); + if (svelte) { + console.log(`\nPlayground: https://svelte.dev/playground?version=commit-${svelte.sha}`); + } + console.log('='.repeat(50)); diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b78346d883..aa32e3c5cd 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -5,6 +5,11 @@ on: branches: - main +concurrency: + # prevent two release workflows from running at once + # race conditions here can result in releases failing + group: ${{ github.workflow }} + permissions: {} jobs: release: @@ -18,13 +23,13 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout Repo - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: # This makes Actions fetch all Git history so that Changesets can generate changelogs with the correct commits fetch-depth: 0 - uses: pnpm/action-setup@v4 - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version: 24.x cache: pnpm diff --git a/benchmarking/benchmarks/reactivity/index.js b/benchmarking/benchmarks/reactivity/index.js index 58b3f5cb29..2b75b3dfc6 100644 --- a/benchmarking/benchmarks/reactivity/index.js +++ b/benchmarking/benchmarks/reactivity/index.js @@ -1,12 +1,5 @@ -import { kairo_avoidable_owned, kairo_avoidable_unowned } from './kairo/kairo_avoidable.js'; -import { kairo_broad_owned, kairo_broad_unowned } from './kairo/kairo_broad.js'; -import { kairo_deep_owned, kairo_deep_unowned } from './kairo/kairo_deep.js'; -import { kairo_diamond_owned, kairo_diamond_unowned } from './kairo/kairo_diamond.js'; -import { kairo_mux_unowned, kairo_mux_owned } from './kairo/kairo_mux.js'; -import { kairo_repeated_unowned, kairo_repeated_owned } from './kairo/kairo_repeated.js'; -import { kairo_triangle_owned, kairo_triangle_unowned } from './kairo/kairo_triangle.js'; -import { kairo_unstable_owned, kairo_unstable_unowned } from './kairo/kairo_unstable.js'; -import { mol_bench_owned, mol_bench_unowned } from './mol_bench.js'; +import fs from 'node:fs'; +import path from 'node:path'; import { sbench_create_0to1, sbench_create_1000to1, @@ -19,10 +12,14 @@ import { sbench_create_4to1, sbench_create_signals } from './sbench.js'; +import { fileURLToPath } from 'node:url'; +import { create_test } from './util.js'; // This benchmark has been adapted from the js-reactivity-benchmark (https://github.com/milomg/js-reactivity-benchmark) // Not all tests are the same, and many parts have been tweaked to capture different data. +const dirname = path.dirname(fileURLToPath(import.meta.url)); + export const reactivity_benchmarks = [ sbench_create_signals, sbench_create_0to1, @@ -33,23 +30,16 @@ export const reactivity_benchmarks = [ sbench_create_1to2, sbench_create_1to4, sbench_create_1to8, - sbench_create_1to1000, - kairo_avoidable_owned, - kairo_avoidable_unowned, - kairo_broad_owned, - kairo_broad_unowned, - kairo_deep_owned, - kairo_deep_unowned, - kairo_diamond_owned, - kairo_diamond_unowned, - kairo_triangle_owned, - kairo_triangle_unowned, - kairo_mux_owned, - kairo_mux_unowned, - kairo_repeated_owned, - kairo_repeated_unowned, - kairo_unstable_owned, - kairo_unstable_unowned, - mol_bench_owned, - mol_bench_unowned + sbench_create_1to1000 ]; + +for (const file of fs.readdirSync(`${dirname}/tests`)) { + if (!file.includes('.bench.')) continue; + + const name = file.replace('.bench.js', ''); + + const module = await import(`${dirname}/tests/${file}`); + const { owned, unowned } = create_test(name, module.default); + + reactivity_benchmarks.push(owned, unowned); +} diff --git a/benchmarking/benchmarks/reactivity/kairo/kairo_avoidable.js b/benchmarking/benchmarks/reactivity/kairo/kairo_avoidable.js deleted file mode 100644 index 9daea6de99..0000000000 --- a/benchmarking/benchmarks/reactivity/kairo/kairo_avoidable.js +++ /dev/null @@ -1,91 +0,0 @@ -import { assert, fastest_test } from '../../../utils.js'; -import * as $ from 'svelte/internal/client'; -import { busy } from './util.js'; - -function setup() { - let head = $.state(0); - let computed1 = $.derived(() => $.get(head)); - let computed2 = $.derived(() => ($.get(computed1), 0)); - let computed3 = $.derived(() => (busy(), $.get(computed2) + 1)); // heavy computation - let computed4 = $.derived(() => $.get(computed3) + 2); - let computed5 = $.derived(() => $.get(computed4) + 3); - - const destroy = $.effect_root(() => { - $.render_effect(() => { - $.get(computed5); - busy(); // heavy side effect - }); - }); - - return { - destroy, - run() { - $.flush(() => { - $.set(head, 1); - }); - assert($.get(computed5) === 6); - for (let i = 0; i < 1000; i++) { - $.flush(() => { - $.set(head, i); - }); - assert($.get(computed5) === 6); - } - } - }; -} - -export async function kairo_avoidable_unowned() { - // Do 10 loops to warm up JIT - for (let i = 0; i < 10; i++) { - const { run, destroy } = setup(); - run(); - destroy(); - } - - const { run, destroy } = setup(); - - const { timing } = await fastest_test(10, () => { - for (let i = 0; i < 1000; i++) { - run(); - } - }); - - destroy(); - - return { - benchmark: 'kairo_avoidable_unowned', - time: timing.time.toFixed(2), - gc_time: timing.gc_time.toFixed(2) - }; -} - -export async function kairo_avoidable_owned() { - let run, destroy; - - const destroy_owned = $.effect_root(() => { - // Do 10 loops to warm up JIT - for (let i = 0; i < 10; i++) { - const { run, destroy } = setup(); - run(); - destroy(); - } - - ({ run, destroy } = setup()); - }); - - const { timing } = await fastest_test(10, () => { - for (let i = 0; i < 1000; i++) { - run(); - } - }); - - // @ts-ignore - destroy(); - destroy_owned(); - - return { - benchmark: 'kairo_avoidable_owned', - time: timing.time.toFixed(2), - gc_time: timing.gc_time.toFixed(2) - }; -} diff --git a/benchmarking/benchmarks/reactivity/kairo/kairo_broad.js b/benchmarking/benchmarks/reactivity/kairo/kairo_broad.js deleted file mode 100644 index 8dc5710c87..0000000000 --- a/benchmarking/benchmarks/reactivity/kairo/kairo_broad.js +++ /dev/null @@ -1,97 +0,0 @@ -import { assert, fastest_test } from '../../../utils.js'; -import * as $ from 'svelte/internal/client'; - -function setup() { - let head = $.state(0); - let last = head; - let counter = 0; - - const destroy = $.effect_root(() => { - for (let i = 0; i < 50; i++) { - let current = $.derived(() => { - return $.get(head) + i; - }); - let current2 = $.derived(() => { - return $.get(current) + 1; - }); - $.render_effect(() => { - $.get(current2); - counter++; - }); - last = current2; - } - }); - - return { - destroy, - run() { - $.flush(() => { - $.set(head, 1); - }); - counter = 0; - for (let i = 0; i < 50; i++) { - $.flush(() => { - $.set(head, i); - }); - assert($.get(last) === i + 50); - } - assert(counter === 50 * 50); - } - }; -} - -export async function kairo_broad_unowned() { - // Do 10 loops to warm up JIT - for (let i = 0; i < 10; i++) { - const { run, destroy } = setup(); - run(); - destroy(); - } - - const { run, destroy } = setup(); - - const { timing } = await fastest_test(10, () => { - for (let i = 0; i < 1000; i++) { - run(); - } - }); - - destroy(); - - return { - benchmark: 'kairo_broad_unowned', - time: timing.time.toFixed(2), - gc_time: timing.gc_time.toFixed(2) - }; -} - -export async function kairo_broad_owned() { - let run, destroy; - - const destroy_owned = $.effect_root(() => { - // Do 10 loops to warm up JIT - for (let i = 0; i < 10; i++) { - const { run, destroy } = setup(); - run(); - destroy(); - } - - ({ run, destroy } = setup()); - }); - - const { timing } = await fastest_test(10, () => { - for (let i = 0; i < 1000; i++) { - run(); - } - }); - - // @ts-ignore - destroy(); - destroy_owned(); - - return { - benchmark: 'kairo_broad_owned', - time: timing.time.toFixed(2), - gc_time: timing.gc_time.toFixed(2) - }; -} diff --git a/benchmarking/benchmarks/reactivity/kairo/kairo_deep.js b/benchmarking/benchmarks/reactivity/kairo/kairo_deep.js deleted file mode 100644 index 8690c85f86..0000000000 --- a/benchmarking/benchmarks/reactivity/kairo/kairo_deep.js +++ /dev/null @@ -1,97 +0,0 @@ -import { assert, fastest_test } from '../../../utils.js'; -import * as $ from 'svelte/internal/client'; - -let len = 50; -const iter = 50; - -function setup() { - let head = $.state(0); - let current = head; - for (let i = 0; i < len; i++) { - let c = current; - current = $.derived(() => { - return $.get(c) + 1; - }); - } - let counter = 0; - - const destroy = $.effect_root(() => { - $.render_effect(() => { - $.get(current); - counter++; - }); - }); - - return { - destroy, - run() { - $.flush(() => { - $.set(head, 1); - }); - counter = 0; - for (let i = 0; i < iter; i++) { - $.flush(() => { - $.set(head, i); - }); - assert($.get(current) === len + i); - } - assert(counter === iter); - } - }; -} - -export async function kairo_deep_unowned() { - // Do 10 loops to warm up JIT - for (let i = 0; i < 10; i++) { - const { run, destroy } = setup(); - run(); - destroy(); - } - - const { run, destroy } = setup(); - - const { timing } = await fastest_test(10, () => { - for (let i = 0; i < 1000; i++) { - run(); - } - }); - - destroy(); - - return { - benchmark: 'kairo_deep_unowned', - time: timing.time.toFixed(2), - gc_time: timing.gc_time.toFixed(2) - }; -} - -export async function kairo_deep_owned() { - let run, destroy; - - const destroy_owned = $.effect_root(() => { - // Do 10 loops to warm up JIT - for (let i = 0; i < 10; i++) { - const { run, destroy } = setup(); - run(); - destroy(); - } - - ({ run, destroy } = setup()); - }); - - const { timing } = await fastest_test(10, () => { - for (let i = 0; i < 1000; i++) { - run(); - } - }); - - // @ts-ignore - destroy(); - destroy_owned(); - - return { - benchmark: 'kairo_deep_owned', - time: timing.time.toFixed(2), - gc_time: timing.gc_time.toFixed(2) - }; -} diff --git a/benchmarking/benchmarks/reactivity/kairo/kairo_diamond.js b/benchmarking/benchmarks/reactivity/kairo/kairo_diamond.js deleted file mode 100644 index bf4e07ee89..0000000000 --- a/benchmarking/benchmarks/reactivity/kairo/kairo_diamond.js +++ /dev/null @@ -1,101 +0,0 @@ -import { assert, fastest_test } from '../../../utils.js'; -import * as $ from 'svelte/internal/client'; - -let width = 5; - -function setup() { - let head = $.state(0); - let current = []; - for (let i = 0; i < width; i++) { - current.push( - $.derived(() => { - return $.get(head) + 1; - }) - ); - } - let sum = $.derived(() => { - return current.map((x) => $.get(x)).reduce((a, b) => a + b, 0); - }); - let counter = 0; - - const destroy = $.effect_root(() => { - $.render_effect(() => { - $.get(sum); - counter++; - }); - }); - - return { - destroy, - run() { - $.flush(() => { - $.set(head, 1); - }); - assert($.get(sum) === 2 * width); - counter = 0; - for (let i = 0; i < 500; i++) { - $.flush(() => { - $.set(head, i); - }); - assert($.get(sum) === (i + 1) * width); - } - assert(counter === 500); - } - }; -} - -export async function kairo_diamond_unowned() { - // Do 10 loops to warm up JIT - for (let i = 0; i < 10; i++) { - const { run, destroy } = setup(); - run(); - destroy(); - } - - const { run, destroy } = setup(); - - const { timing } = await fastest_test(10, () => { - for (let i = 0; i < 1000; i++) { - run(); - } - }); - - destroy(); - - return { - benchmark: 'kairo_diamond_unowned', - time: timing.time.toFixed(2), - gc_time: timing.gc_time.toFixed(2) - }; -} - -export async function kairo_diamond_owned() { - let run, destroy; - - const destroy_owned = $.effect_root(() => { - // Do 10 loops to warm up JIT - for (let i = 0; i < 10; i++) { - const { run, destroy } = setup(); - run(); - destroy(); - } - - ({ run, destroy } = setup()); - }); - - const { timing } = await fastest_test(10, () => { - for (let i = 0; i < 1000; i++) { - run(); - } - }); - - // @ts-ignore - destroy(); - destroy_owned(); - - return { - benchmark: 'kairo_diamond_owned', - time: timing.time.toFixed(2), - gc_time: timing.gc_time.toFixed(2) - }; -} diff --git a/benchmarking/benchmarks/reactivity/kairo/kairo_mux.js b/benchmarking/benchmarks/reactivity/kairo/kairo_mux.js deleted file mode 100644 index fc252a27b5..0000000000 --- a/benchmarking/benchmarks/reactivity/kairo/kairo_mux.js +++ /dev/null @@ -1,94 +0,0 @@ -import { assert, fastest_test } from '../../../utils.js'; -import * as $ from 'svelte/internal/client'; - -function setup() { - let heads = new Array(100).fill(null).map((_) => $.state(0)); - const mux = $.derived(() => { - return Object.fromEntries(heads.map((h) => $.get(h)).entries()); - }); - const splited = heads - .map((_, index) => $.derived(() => $.get(mux)[index])) - .map((x) => $.derived(() => $.get(x) + 1)); - - const destroy = $.effect_root(() => { - splited.forEach((x) => { - $.render_effect(() => { - $.get(x); - }); - }); - }); - - return { - destroy, - run() { - for (let i = 0; i < 10; i++) { - $.flush(() => { - $.set(heads[i], i); - }); - assert($.get(splited[i]) === i + 1); - } - for (let i = 0; i < 10; i++) { - $.flush(() => { - $.set(heads[i], i * 2); - }); - assert($.get(splited[i]) === i * 2 + 1); - } - } - }; -} - -export async function kairo_mux_unowned() { - // Do 10 loops to warm up JIT - for (let i = 0; i < 10; i++) { - const { run, destroy } = setup(); - run(); - destroy(); - } - - const { run, destroy } = setup(); - - const { timing } = await fastest_test(10, () => { - for (let i = 0; i < 1000; i++) { - run(); - } - }); - - destroy(); - - return { - benchmark: 'kairo_mux_unowned', - time: timing.time.toFixed(2), - gc_time: timing.gc_time.toFixed(2) - }; -} - -export async function kairo_mux_owned() { - let run, destroy; - - const destroy_owned = $.effect_root(() => { - // Do 10 loops to warm up JIT - for (let i = 0; i < 10; i++) { - const { run, destroy } = setup(); - run(); - destroy(); - } - - ({ run, destroy } = setup()); - }); - - const { timing } = await fastest_test(10, () => { - for (let i = 0; i < 1000; i++) { - run(); - } - }); - - // @ts-ignore - destroy(); - destroy_owned(); - - return { - benchmark: 'kairo_mux_owned', - time: timing.time.toFixed(2), - gc_time: timing.gc_time.toFixed(2) - }; -} diff --git a/benchmarking/benchmarks/reactivity/kairo/kairo_repeated.js b/benchmarking/benchmarks/reactivity/kairo/kairo_repeated.js deleted file mode 100644 index 3bee06ca0e..0000000000 --- a/benchmarking/benchmarks/reactivity/kairo/kairo_repeated.js +++ /dev/null @@ -1,98 +0,0 @@ -import { assert, fastest_test } from '../../../utils.js'; -import * as $ from 'svelte/internal/client'; - -let size = 30; - -function setup() { - let head = $.state(0); - let current = $.derived(() => { - let result = 0; - for (let i = 0; i < size; i++) { - result += $.get(head); - } - return result; - }); - - let counter = 0; - - const destroy = $.effect_root(() => { - $.render_effect(() => { - $.get(current); - counter++; - }); - }); - - return { - destroy, - run() { - $.flush(() => { - $.set(head, 1); - }); - assert($.get(current) === size); - counter = 0; - for (let i = 0; i < 100; i++) { - $.flush(() => { - $.set(head, i); - }); - assert($.get(current) === i * size); - } - assert(counter === 100); - } - }; -} - -export async function kairo_repeated_unowned() { - // Do 10 loops to warm up JIT - for (let i = 0; i < 10; i++) { - const { run, destroy } = setup(); - run(); - destroy(); - } - - const { run, destroy } = setup(); - - const { timing } = await fastest_test(10, () => { - for (let i = 0; i < 1000; i++) { - run(); - } - }); - - destroy(); - - return { - benchmark: 'kairo_repeated_unowned', - time: timing.time.toFixed(2), - gc_time: timing.gc_time.toFixed(2) - }; -} - -export async function kairo_repeated_owned() { - let run, destroy; - - const destroy_owned = $.effect_root(() => { - // Do 10 loops to warm up JIT - for (let i = 0; i < 10; i++) { - const { run, destroy } = setup(); - run(); - destroy(); - } - - ({ run, destroy } = setup()); - }); - - const { timing } = await fastest_test(10, () => { - for (let i = 0; i < 1000; i++) { - run(); - } - }); - - // @ts-ignore - destroy(); - destroy_owned(); - - return { - benchmark: 'kairo_repeated_owned', - time: timing.time.toFixed(2), - gc_time: timing.gc_time.toFixed(2) - }; -} diff --git a/benchmarking/benchmarks/reactivity/kairo/kairo_triangle.js b/benchmarking/benchmarks/reactivity/kairo/kairo_triangle.js deleted file mode 100644 index 11a419a52e..0000000000 --- a/benchmarking/benchmarks/reactivity/kairo/kairo_triangle.js +++ /dev/null @@ -1,111 +0,0 @@ -import { assert, fastest_test } from '../../../utils.js'; -import * as $ from 'svelte/internal/client'; - -let width = 10; - -function count(number) { - return new Array(number) - .fill(0) - .map((_, i) => i + 1) - .reduce((x, y) => x + y, 0); -} - -function setup() { - let head = $.state(0); - let current = head; - let list = []; - for (let i = 0; i < width; i++) { - let c = current; - list.push(current); - current = $.derived(() => { - return $.get(c) + 1; - }); - } - let sum = $.derived(() => { - return list.map((x) => $.get(x)).reduce((a, b) => a + b, 0); - }); - - let counter = 0; - - const destroy = $.effect_root(() => { - $.render_effect(() => { - $.get(sum); - counter++; - }); - }); - - return { - destroy, - run() { - const constant = count(width); - $.flush(() => { - $.set(head, 1); - }); - assert($.get(sum) === constant); - counter = 0; - for (let i = 0; i < 100; i++) { - $.flush(() => { - $.set(head, i); - }); - assert($.get(sum) === constant - width + i * width); - } - assert(counter === 100); - } - }; -} - -export async function kairo_triangle_unowned() { - // Do 10 loops to warm up JIT - for (let i = 0; i < 10; i++) { - const { run, destroy } = setup(); - run(); - destroy(); - } - - const { run, destroy } = setup(); - - const { timing } = await fastest_test(10, () => { - for (let i = 0; i < 1000; i++) { - run(); - } - }); - - destroy(); - - return { - benchmark: 'kairo_triangle_unowned', - time: timing.time.toFixed(2), - gc_time: timing.gc_time.toFixed(2) - }; -} - -export async function kairo_triangle_owned() { - let run, destroy; - - const destroy_owned = $.effect_root(() => { - // Do 10 loops to warm up JIT - for (let i = 0; i < 10; i++) { - const { run, destroy } = setup(); - run(); - destroy(); - } - - ({ run, destroy } = setup()); - }); - - const { timing } = await fastest_test(10, () => { - for (let i = 0; i < 1000; i++) { - run(); - } - }); - - // @ts-ignore - destroy(); - destroy_owned(); - - return { - benchmark: 'kairo_triangle_owned', - time: timing.time.toFixed(2), - gc_time: timing.gc_time.toFixed(2) - }; -} diff --git a/benchmarking/benchmarks/reactivity/kairo/kairo_unstable.js b/benchmarking/benchmarks/reactivity/kairo/kairo_unstable.js deleted file mode 100644 index 54eb732cb2..0000000000 --- a/benchmarking/benchmarks/reactivity/kairo/kairo_unstable.js +++ /dev/null @@ -1,97 +0,0 @@ -import { assert, fastest_test } from '../../../utils.js'; -import * as $ from 'svelte/internal/client'; - -function setup() { - let head = $.state(0); - const double = $.derived(() => $.get(head) * 2); - const inverse = $.derived(() => -$.get(head)); - let current = $.derived(() => { - let result = 0; - for (let i = 0; i < 20; i++) { - result += $.get(head) % 2 ? $.get(double) : $.get(inverse); - } - return result; - }); - - let counter = 0; - - const destroy = $.effect_root(() => { - $.render_effect(() => { - $.get(current); - counter++; - }); - }); - - return { - destroy, - run() { - $.flush(() => { - $.set(head, 1); - }); - assert($.get(current) === 40); - counter = 0; - for (let i = 0; i < 100; i++) { - $.flush(() => { - $.set(head, i); - }); - } - assert(counter === 100); - } - }; -} - -export async function kairo_unstable_unowned() { - // Do 10 loops to warm up JIT - for (let i = 0; i < 10; i++) { - const { run, destroy } = setup(); - run(); - destroy(); - } - - const { run, destroy } = setup(); - - const { timing } = await fastest_test(10, () => { - for (let i = 0; i < 1000; i++) { - run(); - } - }); - - destroy(); - - return { - benchmark: 'kairo_unstable_unowned', - time: timing.time.toFixed(2), - gc_time: timing.gc_time.toFixed(2) - }; -} - -export async function kairo_unstable_owned() { - let run, destroy; - - const destroy_owned = $.effect_root(() => { - // Do 10 loops to warm up JIT - for (let i = 0; i < 10; i++) { - const { run, destroy } = setup(); - run(); - destroy(); - } - - ({ run, destroy } = setup()); - }); - - const { timing } = await fastest_test(10, () => { - for (let i = 0; i < 1000; i++) { - run(); - } - }); - - // @ts-ignore - destroy(); - destroy_owned(); - - return { - benchmark: 'kairo_unstable_owned', - time: timing.time.toFixed(2), - gc_time: timing.gc_time.toFixed(2) - }; -} diff --git a/benchmarking/benchmarks/reactivity/kairo/util.js b/benchmarking/benchmarks/reactivity/kairo/util.js deleted file mode 100644 index 75e3641ab9..0000000000 --- a/benchmarking/benchmarks/reactivity/kairo/util.js +++ /dev/null @@ -1,6 +0,0 @@ -export function busy() { - let a = 0; - for (let i = 0; i < 1_00; i++) { - a++; - } -} diff --git a/benchmarking/benchmarks/reactivity/sbench.js b/benchmarking/benchmarks/reactivity/sbench.js index ddeaef2514..e197f970c8 100644 --- a/benchmarking/benchmarks/reactivity/sbench.js +++ b/benchmarking/benchmarks/reactivity/sbench.js @@ -1,3 +1,4 @@ +/** @import { Source } from '../../../packages/svelte/src/internal/client/types.js' */ import { fastest_test } from '../../utils.js'; import * as $ from '../../../packages/svelte/src/internal/client/index.js'; @@ -7,360 +8,177 @@ const COUNT = 1e5; * @param {number} n * @param {any[]} sources */ -function create_data_signals(n, sources) { +function create_sources(n, sources) { for (let i = 0; i < n; i++) { sources[i] = $.state(i); } + return sources; } /** - * @param {number} i + * @param {Source} source */ -function create_computation_0(i) { - $.derived(() => i); +function create_derived(source) { + $.derived(() => $.get(source)); } /** - * @param {any} s1 - */ -function create_computation_1(s1) { - $.derived(() => $.get(s1)); -} -/** - * @param {any} s1 - * @param {any} s2 + * + * @param {string} label + * @param {(n: number, sources: Array>) => void} fn + * @param {number} count + * @param {number} num_sources */ -function create_computation_2(s1, s2) { - $.derived(() => $.get(s1) + $.get(s2)); -} +function create_sbench_test(label, count, num_sources, fn) { + return { + label, + fn: async () => { + // Do 3 loops to warm up JIT + for (let i = 0; i < 3; i++) { + fn(count, create_sources(num_sources, [])); + } -function create_computation_1000(ss, offset) { - $.derived(() => { - let sum = 0; - for (let i = 0; i < 1000; i++) { - sum += $.get(ss[offset + i]); + return await fastest_test(10, () => { + const destroy = $.effect_root(() => { + for (let i = 0; i < 10; i++) { + fn(count, create_sources(num_sources, [])); + } + }); + destroy(); + }); } - return sum; - }); + }; } -/** - * @param {number} n - */ -function create_computations_0to1(n) { - for (let i = 0; i < n; i++) { - create_computation_0(i); - } -} +export const sbench_create_signals = create_sbench_test( + 'sbench_create_signals', + COUNT, + COUNT, + create_sources +); -/** - * @param {number} n - * @param {any[]} sources - */ -function create_computations_1to1(n, sources) { - for (let i = 0; i < n; i++) { - const source = sources[i]; - create_computation_1(source); - } -} - -/** - * @param {number} n - * @param {any[]} sources - */ -function create_computations_2to1(n, sources) { +export const sbench_create_0to1 = create_sbench_test('sbench_create_0to1', COUNT, 0, (n) => { for (let i = 0; i < n; i++) { - create_computation_2(sources[i * 2], sources[i * 2 + 1]); + $.derived(() => i); } -} - -function create_computation_4(s1, s2, s3, s4) { - $.derived(() => $.get(s1) + $.get(s2) + $.get(s3) + $.get(s4)); -} +}); -function create_computations_1000to1(n, sources) { - for (let i = 0; i < n; i++) { - create_computation_1000(sources, i * 1000); - } -} - -function create_computations_1to2(n, sources) { - for (let i = 0; i < n / 2; i++) { - const source = sources[i]; - create_computation_1(source); - create_computation_1(source); - } -} - -function create_computations_1to4(n, sources) { - for (let i = 0; i < n / 4; i++) { - const source = sources[i]; - create_computation_1(source); - create_computation_1(source); - create_computation_1(source); - create_computation_1(source); - } -} - -function create_computations_1to8(n, sources) { - for (let i = 0; i < n / 8; i++) { - const source = sources[i]; - create_computation_1(source); - create_computation_1(source); - create_computation_1(source); - create_computation_1(source); - create_computation_1(source); - create_computation_1(source); - create_computation_1(source); - create_computation_1(source); - } -} - -function create_computations_1to1000(n, sources) { - for (let i = 0; i < n / 1000; i++) { - const source = sources[i]; - for (let j = 0; j < 1000; j++) { - create_computation_1(source); +export const sbench_create_1to1 = create_sbench_test( + 'sbench_create_1to1', + COUNT, + COUNT, + (n, sources) => { + for (let i = 0; i < n; i++) { + create_derived(sources[i]); } } -} - -function create_computations_4to1(n, sources) { - for (let i = 0; i < n; i++) { - create_computation_4( - sources[i * 4], - sources[i * 4 + 1], - sources[i * 4 + 2], - sources[i * 4 + 3] - ); - } -} - -/** - * @param {any} fn - * @param {number} count - * @param {number} scount - */ -function bench(fn, count, scount) { - let sources = create_data_signals(scount, []); - - fn(count, sources); -} - -export async function sbench_create_signals() { - // Do 3 loops to warm up JIT - for (let i = 0; i < 3; i++) { - bench(create_data_signals, COUNT, COUNT); - } +); - const { timing } = await fastest_test(10, () => { - for (let i = 0; i < 100; i++) { - bench(create_data_signals, COUNT, COUNT); +export const sbench_create_2to1 = create_sbench_test( + 'sbench_create_2to1', + COUNT / 2, + COUNT, + (n, sources) => { + for (let i = 0; i < n; i++) { + $.derived(() => $.get(sources[i * 2]) + $.get(sources[i * 2 + 1])); } - }); - - return { - benchmark: 'sbench_create_signals', - time: timing.time.toFixed(2), - gc_time: timing.gc_time.toFixed(2) - }; -} - -export async function sbench_create_0to1() { - // Do 3 loops to warm up JIT - for (let i = 0; i < 3; i++) { - bench(create_computations_0to1, COUNT, 0); - } - - const { timing } = await fastest_test(10, () => { - const destroy = $.effect_root(() => { - for (let i = 0; i < 10; i++) { - bench(create_computations_0to1, COUNT, 0); - } - }); - destroy(); - }); - - return { - benchmark: 'sbench_create_0to1', - time: timing.time.toFixed(2), - gc_time: timing.gc_time.toFixed(2) - }; -} - -export async function sbench_create_1to1() { - // Do 3 loops to warm up JIT - for (let i = 0; i < 3; i++) { - bench(create_computations_1to1, COUNT, COUNT); } - - const { timing } = await fastest_test(10, () => { - const destroy = $.effect_root(() => { - for (let i = 0; i < 10; i++) { - bench(create_computations_1to1, COUNT, COUNT); - } - }); - destroy(); - }); - - return { - benchmark: 'sbench_create_1to1', - time: timing.time.toFixed(2), - gc_time: timing.gc_time.toFixed(2) - }; -} - -export async function sbench_create_2to1() { - // Do 3 loops to warm up JIT - for (let i = 0; i < 3; i++) { - bench(create_computations_2to1, COUNT / 2, COUNT); - } - - const { timing } = await fastest_test(10, () => { - const destroy = $.effect_root(() => { - for (let i = 0; i < 10; i++) { - bench(create_computations_2to1, COUNT / 2, COUNT); - } - }); - destroy(); - }); - - return { - benchmark: 'sbench_create_2to1', - time: timing.time.toFixed(2), - gc_time: timing.gc_time.toFixed(2) - }; -} - -export async function sbench_create_4to1() { - // Do 3 loops to warm up JIT - for (let i = 0; i < 3; i++) { - bench(create_computations_4to1, COUNT / 4, COUNT); +); + +export const sbench_create_4to1 = create_sbench_test( + 'sbench_create_4to1', + COUNT / 4, + COUNT, + (n, sources) => { + for (let i = 0; i < n; i++) { + $.derived( + () => + $.get(sources[i * 4]) + + $.get(sources[i * 4 + 1]) + + $.get(sources[i * 4 + 2]) + + $.get(sources[i * 4 + 3]) + ); + } } - - const { timing } = await fastest_test(10, () => { - const destroy = $.effect_root(() => { - for (let i = 0; i < 10; i++) { - bench(create_computations_4to1, COUNT / 4, COUNT); - } - }); - destroy(); - }); - - return { - benchmark: 'sbench_create_4to1', - time: timing.time.toFixed(2), - gc_time: timing.gc_time.toFixed(2) - }; -} - -export async function sbench_create_1000to1() { - // Do 3 loops to warm up JIT - for (let i = 0; i < 3; i++) { - bench(create_computations_1000to1, COUNT / 1000, COUNT); +); + +export const sbench_create_1000to1 = create_sbench_test( + 'sbench_create_1000to1', + COUNT / 1000, + COUNT, + (n, sources) => { + for (let i = 0; i < n; i++) { + const offset = i * 1000; + + $.derived(() => { + let sum = 0; + for (let i = 0; i < 1000; i++) { + sum += $.get(sources[offset + i]); + } + return sum; + }); + } } - - const { timing } = await fastest_test(10, () => { - const destroy = $.effect_root(() => { - for (let i = 0; i < 10; i++) { - bench(create_computations_1000to1, COUNT / 1000, COUNT); - } - }); - destroy(); - }); - - return { - benchmark: 'sbench_create_1000to1', - time: timing.time.toFixed(2), - gc_time: timing.gc_time.toFixed(2) - }; -} - -export async function sbench_create_1to2() { - // Do 3 loops to warm up JIT - for (let i = 0; i < 3; i++) { - bench(create_computations_1to2, COUNT, COUNT / 2); +); + +export const sbench_create_1to2 = create_sbench_test( + 'sbench_create_1to2', + COUNT, + COUNT / 2, + (n, sources) => { + for (let i = 0; i < n / 2; i++) { + const source = sources[i]; + create_derived(source); + create_derived(source); + } } - - const { timing } = await fastest_test(10, () => { - const destroy = $.effect_root(() => { - for (let i = 0; i < 10; i++) { - bench(create_computations_1to2, COUNT, COUNT / 2); - } - }); - destroy(); - }); - - return { - benchmark: 'sbench_create_1to2', - time: timing.time.toFixed(2), - gc_time: timing.gc_time.toFixed(2) - }; -} - -export async function sbench_create_1to4() { - // Do 3 loops to warm up JIT - for (let i = 0; i < 3; i++) { - bench(create_computations_1to4, COUNT, COUNT / 4); +); + +export const sbench_create_1to4 = create_sbench_test( + 'sbench_create_1to4', + COUNT, + COUNT / 4, + (n, sources) => { + for (let i = 0; i < n / 4; i++) { + const source = sources[i]; + create_derived(source); + create_derived(source); + create_derived(source); + create_derived(source); + } } - - const { timing } = await fastest_test(10, () => { - const destroy = $.effect_root(() => { - for (let i = 0; i < 10; i++) { - bench(create_computations_1to4, COUNT, COUNT / 4); - } - }); - destroy(); - }); - - return { - benchmark: 'sbench_create_1to4', - time: timing.time.toFixed(2), - gc_time: timing.gc_time.toFixed(2) - }; -} - -export async function sbench_create_1to8() { - // Do 3 loops to warm up JIT - for (let i = 0; i < 3; i++) { - bench(create_computations_1to8, COUNT, COUNT / 8); +); + +export const sbench_create_1to8 = create_sbench_test( + 'sbench_create_1to8', + COUNT, + COUNT / 8, + (n, sources) => { + for (let i = 0; i < n / 8; i++) { + const source = sources[i]; + create_derived(source); + create_derived(source); + create_derived(source); + create_derived(source); + create_derived(source); + create_derived(source); + create_derived(source); + create_derived(source); + } } - - const { timing } = await fastest_test(10, () => { - const destroy = $.effect_root(() => { - for (let i = 0; i < 10; i++) { - bench(create_computations_1to8, COUNT, COUNT / 8); +); + +export const sbench_create_1to1000 = create_sbench_test( + 'sbench_create_1to1000', + COUNT, + COUNT / 1000, + (n, sources) => { + for (let i = 0; i < n / 1000; i++) { + const source = sources[i]; + for (let j = 0; j < 1000; j++) { + create_derived(source); } - }); - destroy(); - }); - - return { - benchmark: 'sbench_create_1to8', - time: timing.time.toFixed(2), - gc_time: timing.gc_time.toFixed(2) - }; -} - -export async function sbench_create_1to1000() { - // Do 3 loops to warm up JIT - for (let i = 0; i < 3; i++) { - bench(create_computations_1to1000, COUNT, COUNT / 1000); + } } - - const { timing } = await fastest_test(10, () => { - const destroy = $.effect_root(() => { - for (let i = 0; i < 10; i++) { - bench(create_computations_1to1000, COUNT, COUNT / 1000); - } - }); - destroy(); - }); - - return { - benchmark: 'sbench_create_1to1000', - time: timing.time.toFixed(2), - gc_time: timing.gc_time.toFixed(2) - }; -} +); diff --git a/benchmarking/benchmarks/reactivity/tests/kairo_avoidable.bench.js b/benchmarking/benchmarks/reactivity/tests/kairo_avoidable.bench.js new file mode 100644 index 0000000000..d4ba858824 --- /dev/null +++ b/benchmarking/benchmarks/reactivity/tests/kairo_avoidable.bench.js @@ -0,0 +1,35 @@ +import assert from 'node:assert'; +import * as $ from 'svelte/internal/client'; +import { busy } from '../util.js'; + +export default () => { + let head = $.state(0); + let computed1 = $.derived(() => $.get(head)); + let computed2 = $.derived(() => ($.get(computed1), 0)); + let computed3 = $.derived(() => (busy(), $.get(computed2) + 1)); // heavy computation + let computed4 = $.derived(() => $.get(computed3) + 2); + let computed5 = $.derived(() => $.get(computed4) + 3); + + const destroy = $.effect_root(() => { + $.render_effect(() => { + $.get(computed5); + busy(); // heavy side effect + }); + }); + + return { + destroy, + run() { + $.flush(() => { + $.set(head, 1); + }); + assert.equal($.get(computed5), 6); + for (let i = 0; i < 1000; i++) { + $.flush(() => { + $.set(head, i); + }); + assert.equal($.get(computed5), 6); + } + } + }; +}; diff --git a/benchmarking/benchmarks/reactivity/tests/kairo_broad.bench.js b/benchmarking/benchmarks/reactivity/tests/kairo_broad.bench.js new file mode 100644 index 0000000000..aebae7a898 --- /dev/null +++ b/benchmarking/benchmarks/reactivity/tests/kairo_broad.bench.js @@ -0,0 +1,41 @@ +import assert from 'node:assert'; +import * as $ from 'svelte/internal/client'; + +export default () => { + let head = $.state(0); + let last = head; + let counter = 0; + + const destroy = $.effect_root(() => { + for (let i = 0; i < 50; i++) { + let current = $.derived(() => { + return $.get(head) + i; + }); + let current2 = $.derived(() => { + return $.get(current) + 1; + }); + $.render_effect(() => { + $.get(current2); + counter++; + }); + last = current2; + } + }); + + return { + destroy, + run() { + $.flush(() => { + $.set(head, 1); + }); + counter = 0; + for (let i = 0; i < 50; i++) { + $.flush(() => { + $.set(head, i); + }); + assert.equal($.get(last), i + 50); + } + assert.equal(counter, 50 * 50); + } + }; +}; diff --git a/benchmarking/benchmarks/reactivity/tests/kairo_deep.bench.js b/benchmarking/benchmarks/reactivity/tests/kairo_deep.bench.js new file mode 100644 index 0000000000..4a361e9bfc --- /dev/null +++ b/benchmarking/benchmarks/reactivity/tests/kairo_deep.bench.js @@ -0,0 +1,41 @@ +import assert from 'node:assert'; +import * as $ from 'svelte/internal/client'; + +let len = 50; +const iter = 50; + +export default () => { + let head = $.state(0); + let current = head; + for (let i = 0; i < len; i++) { + let c = current; + current = $.derived(() => { + return $.get(c) + 1; + }); + } + let counter = 0; + + const destroy = $.effect_root(() => { + $.render_effect(() => { + $.get(current); + counter++; + }); + }); + + return { + destroy, + run() { + $.flush(() => { + $.set(head, 1); + }); + counter = 0; + for (let i = 0; i < iter; i++) { + $.flush(() => { + $.set(head, i); + }); + assert.equal($.get(current), len + i); + } + assert.equal(counter, iter); + } + }; +}; diff --git a/benchmarking/benchmarks/reactivity/tests/kairo_diamond.bench.js b/benchmarking/benchmarks/reactivity/tests/kairo_diamond.bench.js new file mode 100644 index 0000000000..17d9bd85e5 --- /dev/null +++ b/benchmarking/benchmarks/reactivity/tests/kairo_diamond.bench.js @@ -0,0 +1,45 @@ +import assert from 'node:assert'; +import * as $ from 'svelte/internal/client'; + +let width = 5; + +export default () => { + let head = $.state(0); + let current = []; + for (let i = 0; i < width; i++) { + current.push( + $.derived(() => { + return $.get(head) + 1; + }) + ); + } + let sum = $.derived(() => { + return current.map((x) => $.get(x)).reduce((a, b) => a + b, 0); + }); + let counter = 0; + + const destroy = $.effect_root(() => { + $.render_effect(() => { + $.get(sum); + counter++; + }); + }); + + return { + destroy, + run() { + $.flush(() => { + $.set(head, 1); + }); + assert.equal($.get(sum), 2 * width); + counter = 0; + for (let i = 0; i < 500; i++) { + $.flush(() => { + $.set(head, i); + }); + assert.equal($.get(sum), (i + 1) * width); + } + assert.equal(counter, 500); + } + }; +}; diff --git a/benchmarking/benchmarks/reactivity/tests/kairo_mux.bench.js b/benchmarking/benchmarks/reactivity/tests/kairo_mux.bench.js new file mode 100644 index 0000000000..4af6bf7873 --- /dev/null +++ b/benchmarking/benchmarks/reactivity/tests/kairo_mux.bench.js @@ -0,0 +1,38 @@ +import assert from 'node:assert'; +import * as $ from 'svelte/internal/client'; + +export default () => { + let heads = new Array(100).fill(null).map((_) => $.state(0)); + const mux = $.derived(() => { + return Object.fromEntries(heads.map((h) => $.get(h)).entries()); + }); + const splited = heads + .map((_, index) => $.derived(() => $.get(mux)[index])) + .map((x) => $.derived(() => $.get(x) + 1)); + + const destroy = $.effect_root(() => { + splited.forEach((x) => { + $.render_effect(() => { + $.get(x); + }); + }); + }); + + return { + destroy, + run() { + for (let i = 0; i < 10; i++) { + $.flush(() => { + $.set(heads[i], i); + }); + assert.equal($.get(splited[i]), i + 1); + } + for (let i = 0; i < 10; i++) { + $.flush(() => { + $.set(heads[i], i * 2); + }); + assert.equal($.get(splited[i]), i * 2 + 1); + } + } + }; +}; diff --git a/benchmarking/benchmarks/reactivity/tests/kairo_repeated.bench.js b/benchmarking/benchmarks/reactivity/tests/kairo_repeated.bench.js new file mode 100644 index 0000000000..cab7689fea --- /dev/null +++ b/benchmarking/benchmarks/reactivity/tests/kairo_repeated.bench.js @@ -0,0 +1,42 @@ +import assert from 'node:assert'; +import * as $ from 'svelte/internal/client'; + +let size = 30; + +export default () => { + let head = $.state(0); + let current = $.derived(() => { + let result = 0; + for (let i = 0; i < size; i++) { + result += $.get(head); + } + return result; + }); + + let counter = 0; + + const destroy = $.effect_root(() => { + $.render_effect(() => { + $.get(current); + counter++; + }); + }); + + return { + destroy, + run() { + $.flush(() => { + $.set(head, 1); + }); + assert.equal($.get(current), size); + counter = 0; + for (let i = 0; i < 100; i++) { + $.flush(() => { + $.set(head, i); + }); + assert.equal($.get(current), i * size); + } + assert.equal(counter, 100); + } + }; +}; diff --git a/benchmarking/benchmarks/reactivity/tests/kairo_triangle.bench.js b/benchmarking/benchmarks/reactivity/tests/kairo_triangle.bench.js new file mode 100644 index 0000000000..b4b46c0209 --- /dev/null +++ b/benchmarking/benchmarks/reactivity/tests/kairo_triangle.bench.js @@ -0,0 +1,55 @@ +import assert from 'node:assert'; +import * as $ from 'svelte/internal/client'; + +let width = 10; + +function count(number) { + return new Array(number) + .fill(0) + .map((_, i) => i + 1) + .reduce((x, y) => x + y, 0); +} + +export default () => { + let head = $.state(0); + let current = head; + let list = []; + for (let i = 0; i < width; i++) { + let c = current; + list.push(current); + current = $.derived(() => { + return $.get(c) + 1; + }); + } + let sum = $.derived(() => { + return list.map((x) => $.get(x)).reduce((a, b) => a + b, 0); + }); + + let counter = 0; + + const destroy = $.effect_root(() => { + $.render_effect(() => { + $.get(sum); + counter++; + }); + }); + + return { + destroy, + run() { + const constant = count(width); + $.flush(() => { + $.set(head, 1); + }); + assert.equal($.get(sum), constant); + counter = 0; + for (let i = 0; i < 100; i++) { + $.flush(() => { + $.set(head, i); + }); + assert.equal($.get(sum), constant - width + i * width); + } + assert.equal(counter, 100); + } + }; +}; diff --git a/benchmarking/benchmarks/reactivity/tests/kairo_unstable.bench.js b/benchmarking/benchmarks/reactivity/tests/kairo_unstable.bench.js new file mode 100644 index 0000000000..e7723fae0d --- /dev/null +++ b/benchmarking/benchmarks/reactivity/tests/kairo_unstable.bench.js @@ -0,0 +1,41 @@ +import assert from 'node:assert'; +import * as $ from 'svelte/internal/client'; + +export default () => { + let head = $.state(0); + const double = $.derived(() => $.get(head) * 2); + const inverse = $.derived(() => -$.get(head)); + let current = $.derived(() => { + let result = 0; + for (let i = 0; i < 20; i++) { + result += $.get(head) % 2 ? $.get(double) : $.get(inverse); + } + return result; + }); + + let counter = 0; + + const destroy = $.effect_root(() => { + $.render_effect(() => { + $.get(current); + counter++; + }); + }); + + return { + destroy, + run() { + $.flush(() => { + $.set(head, 1); + }); + assert.equal($.get(current), 40); + counter = 0; + for (let i = 0; i < 100; i++) { + $.flush(() => { + $.set(head, i); + }); + } + assert.equal(counter, 100); + } + }; +}; diff --git a/benchmarking/benchmarks/reactivity/mol_bench.js b/benchmarking/benchmarks/reactivity/tests/mol.bench.js similarity index 53% rename from benchmarking/benchmarks/reactivity/mol_bench.js rename to benchmarking/benchmarks/reactivity/tests/mol.bench.js index 536b078d74..e66f0191d1 100644 --- a/benchmarking/benchmarks/reactivity/mol_bench.js +++ b/benchmarking/benchmarks/reactivity/tests/mol.bench.js @@ -1,4 +1,4 @@ -import { assert, fastest_test } from '../../utils.js'; +import assert from 'node:assert'; import * as $ from 'svelte/internal/client'; /** @@ -18,7 +18,7 @@ function hard(n) { const numbers = Array.from({ length: 5 }, (_, i) => i); -function setup() { +export default () => { let res = []; const A = $.state(0); const B = $.state(0); @@ -59,63 +59,10 @@ function setup() { $.set(A, 2 + i * 2); $.set(B, 2); }); - assert(res[0] === 3198 && res[1] === 1601 && res[2] === 3195 && res[3] === 1598); + assert.equal(res[0], 3198); + assert.equal(res[1], 1601); + assert.equal(res[2], 3195); + assert.equal(res[3], 1598); } }; -} - -export async function mol_bench_owned() { - let run, destroy; - - const destroy_owned = $.effect_root(() => { - // Do 10 loops to warm up JIT - for (let i = 0; i < 10; i++) { - const { run, destroy } = setup(); - run(0); - destroy(); - } - - ({ run, destroy } = setup()); - }); - - const { timing } = await fastest_test(10, () => { - for (let i = 0; i < 1e4; i++) { - run(i); - } - }); - - // @ts-ignore - destroy(); - destroy_owned(); - - return { - benchmark: 'mol_bench_owned', - time: timing.time.toFixed(2), - gc_time: timing.gc_time.toFixed(2) - }; -} - -export async function mol_bench_unowned() { - // Do 10 loops to warm up JIT - for (let i = 0; i < 10; i++) { - const { run, destroy } = setup(); - run(0); - destroy(); - } - - const { run, destroy } = setup(); - - const { timing } = await fastest_test(10, () => { - for (let i = 0; i < 1e4; i++) { - run(i); - } - }); - - destroy(); - - return { - benchmark: 'mol_bench_unowned', - time: timing.time.toFixed(2), - gc_time: timing.gc_time.toFixed(2) - }; -} +}; diff --git a/benchmarking/benchmarks/reactivity/tests/repeated_deps.bench.js b/benchmarking/benchmarks/reactivity/tests/repeated_deps.bench.js new file mode 100644 index 0000000000..a8fbcfdbd6 --- /dev/null +++ b/benchmarking/benchmarks/reactivity/tests/repeated_deps.bench.js @@ -0,0 +1,35 @@ +import assert from 'node:assert'; +import * as $ from 'svelte/internal/client'; + +const ARRAY_SIZE = 1000; + +export default () => { + const signals = Array.from({ length: ARRAY_SIZE }, (_, i) => $.state(i)); + const order = $.state(0); + + // break skipped_deps fast path by changing order of reads + const total = $.derived(() => { + const ord = $.get(order); + let sum = 0; + for (let i = 0; i < ARRAY_SIZE; i++) { + sum += /** @type {number} */ ($.get(signals[(i + ord) % ARRAY_SIZE])); + } + return sum; + }); + + const destroy = $.effect_root(() => { + $.render_effect(() => { + $.get(total); + }); + }); + + return { + destroy, + run() { + for (let i = 0; i < 5; i++) { + $.flush(() => $.set(order, i)); + assert.equal($.get(total), (ARRAY_SIZE * (ARRAY_SIZE - 1)) / 2); // sum of 0..999 + } + } + }; +}; diff --git a/benchmarking/benchmarks/reactivity/util.js b/benchmarking/benchmarks/reactivity/util.js new file mode 100644 index 0000000000..da5e5c51f5 --- /dev/null +++ b/benchmarking/benchmarks/reactivity/util.js @@ -0,0 +1,71 @@ +import * as $ from 'svelte/internal/client'; +import { fastest_test } from '../../utils.js'; + +export function busy() { + let a = 0; + for (let i = 0; i < 1_00; i++) { + a++; + } +} + +/** + * + * @param {string} label + * @param {() => { run: (i?: number) => void, destroy: () => void }} setup + */ +export function create_test(label, setup) { + return { + unowned: { + label: `${label}_unowned`, + fn: async () => { + // Do 10 loops to warm up JIT + for (let i = 0; i < 10; i++) { + const { run, destroy } = setup(); + run(0); + destroy(); + } + + const { run, destroy } = setup(); + + const result = await fastest_test(10, () => { + for (let i = 0; i < 1000; i++) { + run(i); + } + }); + + destroy(); + + return result; + } + }, + owned: { + label: `${label}_owned`, + fn: async () => { + let run, destroy; + + const destroy_owned = $.effect_root(() => { + // Do 10 loops to warm up JIT + for (let i = 0; i < 10; i++) { + const { run, destroy } = setup(); + run(0); + destroy(); + } + + ({ run, destroy } = setup()); + }); + + const result = await fastest_test(10, () => { + for (let i = 0; i < 1000; i++) { + run(i); + } + }); + + // @ts-ignore + destroy(); + destroy_owned(); + + return result; + } + } + }; +} diff --git a/benchmarking/benchmarks/ssr/wrapper/wrapper_bench.js b/benchmarking/benchmarks/ssr/wrapper/wrapper_bench.js index ba0457b80e..9a8dda617d 100644 --- a/benchmarking/benchmarks/ssr/wrapper/wrapper_bench.js +++ b/benchmarking/benchmarks/ssr/wrapper/wrapper_bench.js @@ -1,13 +1,16 @@ +import * as fs from 'node:fs'; +import * as path from 'node:path'; import { render } from 'svelte/server'; -import { fastest_test, read_file, write } from '../../../utils.js'; +import { fastest_test } from '../../../utils.js'; import { compile } from 'svelte/compiler'; const dir = `${process.cwd()}/benchmarking/benchmarks/ssr/wrapper`; async function compile_svelte() { - const output = compile(read_file(`${dir}/App.svelte`), { + const output = compile(read(`${dir}/App.svelte`), { generate: 'server' }); + write(`${dir}/output/App.js`, output.js.code); const module = await import(`${dir}/output/App.js`); @@ -15,22 +18,39 @@ async function compile_svelte() { return module.default; } -export async function wrapper_bench() { - const App = await compile_svelte(); - // Do 3 loops to warm up JIT - for (let i = 0; i < 3; i++) { - render(App); - } +export const wrapper_bench = { + label: 'wrapper_bench', + fn: async () => { + const App = await compile_svelte(); - const { timing } = await fastest_test(10, () => { - for (let i = 0; i < 100; i++) { + // Do 3 loops to warm up JIT + for (let i = 0; i < 3; i++) { render(App); } - }); - return { - benchmark: 'wrapper_bench', - time: timing.time.toFixed(2), - gc_time: timing.gc_time.toFixed(2) - }; + return await fastest_test(10, () => { + for (let i = 0; i < 100; i++) { + render(App); + } + }); + } +}; + +/** + * @param {string} file + */ +function read(file) { + return fs.readFileSync(file, 'utf-8').replace(/\r\n/g, '\n'); +} + +/** + * @param {string} file + * @param {string} contents + */ +function write(file, contents) { + try { + fs.mkdirSync(path.dirname(file), { recursive: true }); + } catch {} + + fs.writeFileSync(file, contents); } diff --git a/benchmarking/compare/runner.js b/benchmarking/compare/runner.js index a2e8646379..11e40ed983 100644 --- a/benchmarking/compare/runner.js +++ b/benchmarking/compare/runner.js @@ -1,10 +1,13 @@ import { reactivity_benchmarks } from '../benchmarks/reactivity/index.js'; const results = []; -for (const benchmark of reactivity_benchmarks) { - const result = await benchmark(); - console.error(result.benchmark); - results.push(result); + +for (let i = 0; i < reactivity_benchmarks.length; i += 1) { + const benchmark = reactivity_benchmarks[i]; + + process.stderr.write(`Running ${i + 1}/${reactivity_benchmarks.length} ${benchmark.label} `); + results.push({ benchmark: benchmark.label, ...(await benchmark.fn()) }); + process.stderr.write('\x1b[2K\r'); } process.send(results); diff --git a/benchmarking/run.js b/benchmarking/run.js index bd96b9c2dc..2b09f7c592 100644 --- a/benchmarking/run.js +++ b/benchmarking/run.js @@ -2,54 +2,86 @@ import * as $ from '../packages/svelte/src/internal/client/index.js'; import { reactivity_benchmarks } from './benchmarks/reactivity/index.js'; import { ssr_benchmarks } from './benchmarks/ssr/index.js'; -let total_time = 0; -let total_gc_time = 0; +// e.g. `pnpm bench kairo` to only run the kairo benchmarks +const filters = process.argv.slice(2); const suites = [ - { benchmarks: reactivity_benchmarks, name: 'reactivity benchmarks' }, - { benchmarks: ssr_benchmarks, name: 'server-side rendering benchmarks' } -]; + { + benchmarks: reactivity_benchmarks.filter( + (b) => filters.length === 0 || filters.some((f) => b.label.includes(f)) + ), + name: 'reactivity benchmarks' + }, + { + benchmarks: ssr_benchmarks.filter( + (b) => filters.length === 0 || filters.some((f) => b.label.includes(f)) + ), + name: 'server-side rendering benchmarks' + } +].filter((suite) => suite.benchmarks.length > 0); + +if (suites.length === 0) { + console.log('No benchmarks matched provided filters'); + process.exit(1); +} + +const COLUMN_WIDTHS = [25, 9, 9]; +const TOTAL_WIDTH = COLUMN_WIDTHS.reduce((a, b) => a + b); + +const pad_right = (str, n) => str + ' '.repeat(n - str.length); +const pad_left = (str, n) => ' '.repeat(n - str.length) + str; + +let total_time = 0; +let total_gc_time = 0; -// eslint-disable-next-line no-console -console.log('\x1b[1m', '-- Benchmarking Started --', '\x1b[0m'); $.push({}, true); + try { for (const { benchmarks, name } of suites) { let suite_time = 0; let suite_gc_time = 0; - // eslint-disable-next-line no-console + console.log(`\nRunning ${name}...\n`); + console.log( + pad_right('Benchmark', COLUMN_WIDTHS[0]) + + pad_left('Time', COLUMN_WIDTHS[1]) + + pad_left('GC time', COLUMN_WIDTHS[2]) + ); + console.log('='.repeat(TOTAL_WIDTH)); for (const benchmark of benchmarks) { - const results = await benchmark(); - // eslint-disable-next-line no-console - console.log(results); - total_time += Number(results.time); - total_gc_time += Number(results.gc_time); - suite_time += Number(results.time); - suite_gc_time += Number(results.gc_time); + const results = await benchmark.fn(); + console.log( + pad_right(benchmark.label, COLUMN_WIDTHS[0]) + + pad_left(results.time.toFixed(2), COLUMN_WIDTHS[1]) + + pad_left(results.gc_time.toFixed(2), COLUMN_WIDTHS[2]) + ); + total_time += results.time; + total_gc_time += results.gc_time; + suite_time += results.time; + suite_gc_time += results.gc_time; } - console.log(`\nFinished ${name}.\n`); - - // eslint-disable-next-line no-console - console.log({ - suite_time: suite_time.toFixed(2), - suite_gc_time: suite_gc_time.toFixed(2) - }); + console.log('='.repeat(TOTAL_WIDTH)); + console.log( + pad_right('suite', COLUMN_WIDTHS[0]) + + pad_left(suite_time.toFixed(2), COLUMN_WIDTHS[1]) + + pad_left(suite_gc_time.toFixed(2), COLUMN_WIDTHS[2]) + ); + console.log('='.repeat(TOTAL_WIDTH)); } } catch (e) { - // eslint-disable-next-line no-console - console.log('\x1b[1m', '\n-- Benchmarking Failed --\n', '\x1b[0m'); // eslint-disable-next-line no-console console.error(e); process.exit(1); } + $.pop(); -// eslint-disable-next-line no-console -console.log('\x1b[1m', '\n-- Benchmarking Complete --\n', '\x1b[0m'); -// eslint-disable-next-line no-console -console.log({ - total_time: total_time.toFixed(2), - total_gc_time: total_gc_time.toFixed(2) -}); + +console.log(''); + +console.log( + pad_right('total', COLUMN_WIDTHS[0]) + + pad_left(total_time.toFixed(2), COLUMN_WIDTHS[1]) + + pad_left(total_gc_time.toFixed(2), COLUMN_WIDTHS[2]) +); diff --git a/benchmarking/utils.js b/benchmarking/utils.js index 684d2ee02b..5581135e00 100644 --- a/benchmarking/utils.js +++ b/benchmarking/utils.js @@ -1,78 +1,30 @@ import { performance, PerformanceObserver } from 'node:perf_hooks'; import v8 from 'v8-natives'; -import * as fs from 'node:fs'; -import * as path from 'node:path'; // Credit to https://github.com/milomg/js-reactivity-benchmark for the logic for timing + GC tracking. -class GarbageTrack { - track_id = 0; - observer = new PerformanceObserver((list) => this.perf_entries.push(...list.getEntries())); - perf_entries = []; - periods = []; +async function track(fn) { + v8.collectGarbage(); - watch(fn) { - this.track_id++; - const start = performance.now(); - const result = fn(); - const end = performance.now(); - this.periods.push({ track_id: this.track_id, start, end }); + /** @type {PerformanceEntry[]} */ + const entries = []; - return { result, track_id: this.track_id }; - } + const observer = new PerformanceObserver((list) => entries.push(...list.getEntries())); + observer.observe({ entryTypes: ['gc'] }); - /** - * @param {number} track_id - */ - async gcDuration(track_id) { - await promise_delay(10); + const start = performance.now(); + fn(); + const end = performance.now(); - const period = this.periods.find((period) => period.track_id === track_id); - if (!period) { - // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors - return Promise.reject('no period found'); - } + await new Promise((f) => setTimeout(f, 10)); - const entries = this.perf_entries.filter( - (e) => e.startTime >= period.start && e.startTime < period.end - ); - return entries.reduce((t, e) => e.duration + t, 0); - } + const gc_time = entries + .filter((e) => e.startTime >= start && e.startTime < end) + .reduce((t, e) => e.duration + t, 0); - destroy() { - this.observer.disconnect(); - } + observer.disconnect(); - constructor() { - this.observer.observe({ entryTypes: ['gc'] }); - } -} - -function promise_delay(timeout = 0) { - return new Promise((resolve) => setTimeout(resolve, timeout)); -} - -/** - * @param {{ (): void; (): any; }} fn - */ -function run_timed(fn) { - const start = performance.now(); - const result = fn(); - const time = performance.now() - start; - return { result, time }; -} - -/** - * @param {() => void} fn - */ -async function run_tracked(fn) { - v8.collectGarbage(); - const gc_track = new GarbageTrack(); - const { result: wrappedResult, track_id } = gc_track.watch(() => run_timed(fn)); - const gc_time = await gc_track.gcDuration(track_id); - const { result, time } = wrappedResult; - gc_track.destroy(); - return { result, timing: { time, gc_time } }; + return { time: end - start, gc_time }; } /** @@ -80,40 +32,12 @@ async function run_tracked(fn) { * @param {() => void} fn */ export async function fastest_test(times, fn) { + /** @type {Array<{ time: number, gc_time: number }>} */ const results = []; - for (let i = 0; i < times; i++) { - const run = await run_tracked(fn); - results.push(run); - } - const fastest = results.reduce((a, b) => (a.timing.time < b.timing.time ? a : b)); - return fastest; -} - -/** - * @param {boolean} a - */ -export function assert(a) { - if (!a) { - throw new Error('Assertion failed'); + for (let i = 0; i < times; i++) { + results.push(await track(fn)); } -} - -/** - * @param {string} file - */ -export function read_file(file) { - return fs.readFileSync(file, 'utf-8').replace(/\r\n/g, '\n'); -} - -/** - * @param {string} file - * @param {string} contents - */ -export function write(file, contents) { - try { - fs.mkdirSync(path.dirname(file), { recursive: true }); - } catch {} - fs.writeFileSync(file, contents); + return results.reduce((a, b) => (a.time < b.time ? a : b)); } diff --git a/documentation/docs/01-introduction/02-getting-started.md b/documentation/docs/01-introduction/02-getting-started.md index 519959a0b1..ecb1055443 100644 --- a/documentation/docs/01-introduction/02-getting-started.md +++ b/documentation/docs/01-introduction/02-getting-started.md @@ -15,7 +15,7 @@ Don't worry if you don't know Svelte yet! You can ignore all the nice features S ## Alternatives to SvelteKit -You can also use Svelte directly with Vite by running `npm create vite@latest` and selecting the `svelte` option. With this, `npm run build` will generate HTML, JS, and CSS files inside the `dist` directory using [vite-plugin-svelte](https://github.com/sveltejs/vite-plugin-svelte). In most cases, you will probably need to [choose a routing library](/packages#routing) as well. +You can also use Svelte directly with Vite via [vite-plugin-svelte](https://github.com/sveltejs/vite-plugin-svelte) by running `npm create vite@latest` and selecting the `svelte` option (or, if working with an existing project, adding the plugin to your `vite.config.js` file). With this, `npm run build` will generate HTML, JS, and CSS files inside the `dist` directory. In most cases, you will probably need to [choose a routing library](/packages#routing) as well. >[!NOTE] Vite is often used in standalone mode to build [single page apps (SPAs)](../kit/glossary#SPA), which you can also [build with SvelteKit](../kit/single-page-apps). @@ -25,7 +25,7 @@ There are also [plugins for other bundlers](/packages#bundler-plugins), but we r The Svelte team maintains a [VS Code extension](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode), and there are integrations with various other [editors](https://sveltesociety.dev/collection/editor-support-c85c080efc292a34) and tools as well. -You can also check your code from the command line using [`sv check`](https://github.com/sveltejs/cli). +You can also check your code from the command line using [`npx sv check`](https://svelte.dev/docs/cli/sv-check). ## Getting help diff --git a/documentation/docs/02-runes/02-$state.md b/documentation/docs/02-runes/02-$state.md index 6fbf3b8895..d763b6578f 100644 --- a/documentation/docs/02-runes/02-$state.md +++ b/documentation/docs/02-runes/02-$state.md @@ -1,5 +1,6 @@ --- title: $state +tags: rune-state --- The `$state` rune allows you to create _reactive state_, which means that your UI _reacts_ when it changes. diff --git a/documentation/docs/02-runes/03-$derived.md b/documentation/docs/02-runes/03-$derived.md index d3e46eb22d..35cb6c1912 100644 --- a/documentation/docs/02-runes/03-$derived.md +++ b/documentation/docs/02-runes/03-$derived.md @@ -1,5 +1,6 @@ --- title: $derived +tags: rune-derived --- Derived state is declared with the `$derived` rune: diff --git a/documentation/docs/02-runes/04-$effect.md b/documentation/docs/02-runes/04-$effect.md index 6c42f55795..d41c5b8e6a 100644 --- a/documentation/docs/02-runes/04-$effect.md +++ b/documentation/docs/02-runes/04-$effect.md @@ -1,5 +1,6 @@ --- title: $effect +tags: rune-effect --- Effects are functions that run when state updates, and can be used for things like calling third-party libraries, drawing on `` elements, or making network requests. They only run in the browser, not during server-side rendering. diff --git a/documentation/docs/02-runes/05-$props.md b/documentation/docs/02-runes/05-$props.md index 222b4831b6..737cdf523e 100644 --- a/documentation/docs/02-runes/05-$props.md +++ b/documentation/docs/02-runes/05-$props.md @@ -1,5 +1,6 @@ --- title: $props +tags: rune-props --- The inputs to a component are referred to as _props_, which is short for _properties_. You pass props to components just like you pass attributes to elements: @@ -198,6 +199,8 @@ You can, of course, separate the type declaration from the annotation: > [!NOTE] Interfaces for native DOM elements are provided in the `svelte/elements` module (see [Typing wrapper components](typescript#Typing-wrapper-components)) +If your component exposes [snippet](snippet) props like `children`, these should be typed using the `Snippet` interface imported from `'svelte'` — see [Typing snippets](snippet#Typing-snippets) for examples. + Adding types is recommended, as it ensures that people using your component can easily discover which props they should provide. diff --git a/documentation/docs/02-runes/07-$inspect.md b/documentation/docs/02-runes/07-$inspect.md index 6d47e30e27..f67e250b45 100644 --- a/documentation/docs/02-runes/07-$inspect.md +++ b/documentation/docs/02-runes/07-$inspect.md @@ -1,5 +1,6 @@ --- title: $inspect +tags: rune-inspect --- > [!NOTE] `$inspect` only works during development. In a production build it becomes a noop. diff --git a/documentation/docs/03-template-syntax/02-if.md b/documentation/docs/03-template-syntax/02-if.md index 1378733e6f..499f6d219c 100644 --- a/documentation/docs/03-template-syntax/02-if.md +++ b/documentation/docs/03-template-syntax/02-if.md @@ -1,5 +1,6 @@ --- title: {#if ...} +tags: template-if --- ```svelte diff --git a/documentation/docs/03-template-syntax/03-each.md b/documentation/docs/03-template-syntax/03-each.md index 57ed0def71..424e16a18a 100644 --- a/documentation/docs/03-template-syntax/03-each.md +++ b/documentation/docs/03-template-syntax/03-each.md @@ -1,5 +1,6 @@ --- title: {#each ...} +tags: template-each --- ```svelte diff --git a/documentation/docs/03-template-syntax/04-key.md b/documentation/docs/03-template-syntax/04-key.md index 10b6ab4358..6c88e28f86 100644 --- a/documentation/docs/03-template-syntax/04-key.md +++ b/documentation/docs/03-template-syntax/04-key.md @@ -1,5 +1,6 @@ --- title: {#key ...} +tags: template-key --- ```svelte diff --git a/documentation/docs/03-template-syntax/05-await.md b/documentation/docs/03-template-syntax/05-await.md index 842b3c7e32..aebaf8d33a 100644 --- a/documentation/docs/03-template-syntax/05-await.md +++ b/documentation/docs/03-template-syntax/05-await.md @@ -1,5 +1,6 @@ --- title: {#await ...} +tags: template-await --- ```svelte diff --git a/documentation/docs/03-template-syntax/08-@html.md b/documentation/docs/03-template-syntax/08-@html.md index 6d8a8be0c6..36b8ad36b3 100644 --- a/documentation/docs/03-template-syntax/08-@html.md +++ b/documentation/docs/03-template-syntax/08-@html.md @@ -1,5 +1,6 @@ --- title: {@html ...} +tags: template-html --- To inject raw HTML into your component, use the `{@html ...}` tag: diff --git a/documentation/docs/03-template-syntax/09-@attach.md b/documentation/docs/03-template-syntax/09-@attach.md index b25fbb32a6..0087923b15 100644 --- a/documentation/docs/03-template-syntax/09-@attach.md +++ b/documentation/docs/03-template-syntax/09-@attach.md @@ -1,5 +1,6 @@ --- title: {@attach ...} +tags: attachments --- Attachments are functions that run in an [effect]($effect) when an element is mounted to the DOM or when [state]($state) read inside the function updates. @@ -82,6 +83,14 @@ Attachments can also be created inline ([demo](/playground/untitled#H4sIAAAAAAAA > [!NOTE] > The nested effect runs whenever `color` changes, while the outer effect (where `canvas.getContext(...)` is called) only runs once, since it doesn't read any reactive state. +## Conditional attachments + +Falsy values like `false` or `undefined` are treated as no attachment, enabling conditional usage: + +```svelte +
...
+``` + ## Passing attachments to components When used on a component, `{@attach ...}` will create a prop whose key is a [`Symbol`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol). If the component then [spreads](/tutorial/svelte/spread-props) props onto an element, the element will receive those attachments. diff --git a/documentation/docs/03-template-syntax/14-transition.md b/documentation/docs/03-template-syntax/14-transition.md index c51175c272..9b14b7b25b 100644 --- a/documentation/docs/03-template-syntax/14-transition.md +++ b/documentation/docs/03-template-syntax/14-transition.md @@ -1,5 +1,6 @@ --- title: transition: +tags: transitions --- A _transition_ is triggered by an element entering or leaving the DOM as a result of a state change. diff --git a/documentation/docs/03-template-syntax/15-in-and-out.md b/documentation/docs/03-template-syntax/15-in-and-out.md index f4e37c845e..8adb62bf14 100644 --- a/documentation/docs/03-template-syntax/15-in-and-out.md +++ b/documentation/docs/03-template-syntax/15-in-and-out.md @@ -1,5 +1,6 @@ --- title: in: and out: +tags: transitions --- The `in:` and `out:` directives are identical to [`transition:`](transition), except that the resulting transitions are not bidirectional — an `in` transition will continue to 'play' alongside the `out` transition, rather than reversing, if the block is outroed while the transition is in progress. If an out transition is aborted, transitions will restart from scratch. @@ -7,7 +8,7 @@ The `in:` and `out:` directives are identical to [`transition:`](transition), ex ```svelte diff --git a/documentation/docs/03-template-syntax/17-style.md b/documentation/docs/03-template-syntax/17-style.md index aa61cdcde3..8b25c221d6 100644 --- a/documentation/docs/03-template-syntax/17-style.md +++ b/documentation/docs/03-template-syntax/17-style.md @@ -1,5 +1,6 @@ --- title: style: +tags: template-style --- The `style:` directive provides a shorthand for setting multiple styles on an element. diff --git a/documentation/docs/03-template-syntax/18-class.md b/documentation/docs/03-template-syntax/18-class.md index db85db4b37..5d7cb3100d 100644 --- a/documentation/docs/03-template-syntax/18-class.md +++ b/documentation/docs/03-template-syntax/18-class.md @@ -1,5 +1,6 @@ --- title: class +tags: template-style --- There are two ways to set classes on elements: the `class` attribute, and the `class:` directive. diff --git a/documentation/docs/04-styling/01-scoped-styles.md b/documentation/docs/04-styling/01-scoped-styles.md index eae26d0cb1..cdfbeab19b 100644 --- a/documentation/docs/04-styling/01-scoped-styles.md +++ b/documentation/docs/04-styling/01-scoped-styles.md @@ -1,5 +1,6 @@ --- title: Scoped styles +tags: styles-scoped --- Svelte components can include a ` \ No newline at end of file diff --git a/packages/svelte/tests/html_equal.js b/packages/svelte/tests/html_equal.js index 76a4a957a5..cdb8f52e37 100644 --- a/packages/svelte/tests/html_equal.js +++ b/packages/svelte/tests/html_equal.js @@ -9,7 +9,9 @@ function clean_children(node, opts) { let previous = null; let has_element_children = false; let template = - node.nodeName === 'TEMPLATE' ? /** @type {HTMLTemplateElement} */ (node) : undefined; + node.nodeName.toUpperCase() === 'TEMPLATE' + ? /** @type {HTMLTemplateElement} */ (node) + : undefined; if (template) { const div = document.createElement('div'); @@ -23,22 +25,20 @@ function clean_children(node, opts) { }); attributes.forEach((attr) => { - node.removeAttribute(attr.name); + if (attr.name !== 'xmlns') node.removeAttribute(attr.name); }); - attributes.forEach((attr) => { + attributes.forEach(({ name, value }) => { // Strip out the special onload/onerror hydration events from the test output - if ((attr.name === 'onload' || attr.name === 'onerror') && attr.value === 'this.__e=event') { + if (['onload', 'onerror', 'xmlns'].includes(name) && value === 'this.__e=event') { return; } - let value = attr.value; - - if (attr.name === 'class') { + if (name === 'class') { value = value.replace(/svelte-\w+/, 'svelte-xyz123'); } - node.setAttribute(attr.name, value); + node.setAttribute(name, value); }); for (let child of [...node.childNodes]) { diff --git a/packages/svelte/tests/hydration/samples/no-reset-debug/_config.js b/packages/svelte/tests/hydration/samples/no-reset-debug/_config.js new file mode 100644 index 0000000000..2bdb29e436 --- /dev/null +++ b/packages/svelte/tests/hydration/samples/no-reset-debug/_config.js @@ -0,0 +1,15 @@ +import { test } from '../../test'; + +/** @type {typeof console.log} */ +let log; + +export default test({ + before_test() { + log = console.log; + console.log = () => {}; + }, + + after_test() { + console.log = log; + } +}); diff --git a/packages/svelte/tests/hydration/samples/no-reset-debug/main.svelte b/packages/svelte/tests/hydration/samples/no-reset-debug/main.svelte new file mode 100644 index 0000000000..915aa0152d --- /dev/null +++ b/packages/svelte/tests/hydration/samples/no-reset-debug/main.svelte @@ -0,0 +1,8 @@ + + +
+ {@debug test} + something +
\ No newline at end of file diff --git a/packages/svelte/tests/hydration/samples/no-reset-snippet/_config.js b/packages/svelte/tests/hydration/samples/no-reset-snippet/_config.js new file mode 100644 index 0000000000..f47bee71df --- /dev/null +++ b/packages/svelte/tests/hydration/samples/no-reset-snippet/_config.js @@ -0,0 +1,3 @@ +import { test } from '../../test'; + +export default test({}); diff --git a/packages/svelte/tests/hydration/samples/no-reset-snippet/main.svelte b/packages/svelte/tests/hydration/samples/no-reset-snippet/main.svelte new file mode 100644 index 0000000000..c6668c0684 --- /dev/null +++ b/packages/svelte/tests/hydration/samples/no-reset-snippet/main.svelte @@ -0,0 +1,6 @@ +
+ {#snippet test()} + + {/snippet} + something +
\ No newline at end of file diff --git a/packages/svelte/tests/hydration/samples/optgroup-rich-content/_config.js b/packages/svelte/tests/hydration/samples/optgroup-rich-content/_config.js new file mode 100644 index 0000000000..36ca2e6ae5 --- /dev/null +++ b/packages/svelte/tests/hydration/samples/optgroup-rich-content/_config.js @@ -0,0 +1,31 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + // This test verifies that hydration works correctly for + // optgroup elements with rich HTML content (non-option elements inside optgroup) + snapshot(target) { + const select = target.querySelector('select'); + + return { + select + }; + }, + + async test(assert, target) { + const optgroup = target.querySelector('optgroup'); + const options = target.querySelectorAll('option'); + const button = target.querySelector('button'); + + // Check options content - the span inside optgroup gets stripped but text remains + assert.equal(options[0]?.textContent, 'hello hello'); + assert.equal(options[1]?.textContent, 'Plain option'); + + // Update via button click + flushSync(() => { + button?.click(); + }); + + assert.equal(options[0]?.textContent, 'changed changed'); + } +}); diff --git a/packages/svelte/tests/hydration/samples/optgroup-rich-content/_expected.html b/packages/svelte/tests/hydration/samples/optgroup-rich-content/_expected.html new file mode 100644 index 0000000000..911b92770b --- /dev/null +++ b/packages/svelte/tests/hydration/samples/optgroup-rich-content/_expected.html @@ -0,0 +1 @@ + diff --git a/packages/svelte/tests/hydration/samples/optgroup-rich-content/main.svelte b/packages/svelte/tests/hydration/samples/optgroup-rich-content/main.svelte new file mode 100644 index 0000000000..227c4d56d5 --- /dev/null +++ b/packages/svelte/tests/hydration/samples/optgroup-rich-content/main.svelte @@ -0,0 +1,16 @@ + + + + + diff --git a/packages/svelte/tests/hydration/samples/option-rich-content-continues/_config.js b/packages/svelte/tests/hydration/samples/option-rich-content-continues/_config.js new file mode 100644 index 0000000000..a2163b7c67 --- /dev/null +++ b/packages/svelte/tests/hydration/samples/option-rich-content-continues/_config.js @@ -0,0 +1,34 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + // This test verifies that hydration continues correctly after + // an option element with rich HTML content + snapshot(target) { + const select = target.querySelector('select'); + const options = target.querySelectorAll('option'); + const p = target.querySelector('p'); + const button = target.querySelector('button'); + + return { + select, + option1: options[0], + option2: options[1], + p, + button + }; + }, + + async test(assert, target) { + const option = target.querySelector('option'); + const button = target.querySelector('button'); + + assert.equal(option?.textContent, 'hello hello'); + + flushSync(() => { + button?.click(); + }); + + assert.equal(option?.textContent, 'changed changed'); + } +}); diff --git a/packages/svelte/tests/hydration/samples/option-rich-content-continues/_expected.html b/packages/svelte/tests/hydration/samples/option-rich-content-continues/_expected.html new file mode 100644 index 0000000000..d3e209c990 --- /dev/null +++ b/packages/svelte/tests/hydration/samples/option-rich-content-continues/_expected.html @@ -0,0 +1 @@ + diff --git a/packages/svelte/tests/hydration/samples/option-rich-content-continues/main.svelte b/packages/svelte/tests/hydration/samples/option-rich-content-continues/main.svelte new file mode 100644 index 0000000000..47edc28035 --- /dev/null +++ b/packages/svelte/tests/hydration/samples/option-rich-content-continues/main.svelte @@ -0,0 +1,12 @@ + + + + + diff --git a/packages/svelte/tests/hydration/samples/option-rich-content-static/_config.js b/packages/svelte/tests/hydration/samples/option-rich-content-static/_config.js new file mode 100644 index 0000000000..ca8c2b6ec1 --- /dev/null +++ b/packages/svelte/tests/hydration/samples/option-rich-content-static/_config.js @@ -0,0 +1,39 @@ +import { test } from '../../test'; + +export default test({ + // This test verifies that completely static select with rich option content + // hydrates correctly and the content is preserved + snapshot(target) { + const select = target.querySelector('select'); + const options = target.querySelectorAll('option'); + + return { + select, + option1: options[0], + option2: options[1], + option3: options[2] + }; + }, + + async test(assert, target) { + const options = target.querySelectorAll('option'); + + // Verify the rich content is present in the options + assert.equal(options[0]?.textContent, 'Bold Option'); + assert.equal(options[1]?.textContent, 'Italic Option'); + assert.equal(options[2]?.textContent, 'Plain Option'); + + // Check that the rich elements are actually there (on supporting browsers) + const strong = options[0]?.querySelector('strong'); + const em = options[1]?.querySelector('em'); + + // These may or may not exist depending on browser support + // but the text content should always be correct + if (strong) { + assert.equal(strong.textContent, 'Bold'); + } + if (em) { + assert.equal(em.textContent, 'Italic'); + } + } +}); diff --git a/packages/svelte/tests/hydration/samples/option-rich-content-static/_expected.html b/packages/svelte/tests/hydration/samples/option-rich-content-static/_expected.html new file mode 100644 index 0000000000..93cfa49dd1 --- /dev/null +++ b/packages/svelte/tests/hydration/samples/option-rich-content-static/_expected.html @@ -0,0 +1 @@ + diff --git a/packages/svelte/tests/hydration/samples/option-rich-content-static/main.svelte b/packages/svelte/tests/hydration/samples/option-rich-content-static/main.svelte new file mode 100644 index 0000000000..bd81687434 --- /dev/null +++ b/packages/svelte/tests/hydration/samples/option-rich-content-static/main.svelte @@ -0,0 +1,5 @@ + diff --git a/packages/svelte/tests/hydration/samples/rich-select/Option.svelte b/packages/svelte/tests/hydration/samples/rich-select/Option.svelte new file mode 100644 index 0000000000..783915d571 --- /dev/null +++ b/packages/svelte/tests/hydration/samples/rich-select/Option.svelte @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/svelte/tests/hydration/samples/rich-select/_config.js b/packages/svelte/tests/hydration/samples/rich-select/_config.js new file mode 100644 index 0000000000..f47bee71df --- /dev/null +++ b/packages/svelte/tests/hydration/samples/rich-select/_config.js @@ -0,0 +1,3 @@ +import { test } from '../../test'; + +export default test({}); diff --git a/packages/svelte/tests/hydration/samples/rich-select/main.svelte b/packages/svelte/tests/hydration/samples/rich-select/main.svelte new file mode 100644 index 0000000000..bb77d8e57a --- /dev/null +++ b/packages/svelte/tests/hydration/samples/rich-select/main.svelte @@ -0,0 +1,173 @@ + + + + + + + + + + + + + + + +{#snippet opt()} + +{/snippet} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +{#snippet option_snippet()} + +{/snippet} + + + + + + + + + +{#snippet option_snippet2()} + +{/snippet} + + + + + + + + + +{#snippet conditional_option()} + +{/snippet} + + + + + + + diff --git a/packages/svelte/tests/hydration/samples/safari-borking-2/Child.svelte b/packages/svelte/tests/hydration/samples/safari-borking-2/Child.svelte new file mode 100644 index 0000000000..a62bd4ec90 --- /dev/null +++ b/packages/svelte/tests/hydration/samples/safari-borking-2/Child.svelte @@ -0,0 +1,6 @@ + + +
+{message} diff --git a/packages/svelte/tests/hydration/samples/safari-borking-2/_config.js b/packages/svelte/tests/hydration/samples/safari-borking-2/_config.js new file mode 100644 index 0000000000..cf22ff2c85 --- /dev/null +++ b/packages/svelte/tests/hydration/samples/safari-borking-2/_config.js @@ -0,0 +1,5 @@ +import { test } from '../../test'; + +export default test({ + expect_hydration_error: true +}); diff --git a/packages/svelte/tests/hydration/samples/safari-borking-2/_override.html b/packages/svelte/tests/hydration/samples/safari-borking-2/_override.html new file mode 100644 index 0000000000..17ec01970a --- /dev/null +++ b/packages/svelte/tests/hydration/samples/safari-borking-2/_override.html @@ -0,0 +1 @@ +
call +636-555-3226 now

42

diff --git a/packages/svelte/tests/hydration/samples/safari-borking-2/main.svelte b/packages/svelte/tests/hydration/samples/safari-borking-2/main.svelte new file mode 100644 index 0000000000..daa25067ec --- /dev/null +++ b/packages/svelte/tests/hydration/samples/safari-borking-2/main.svelte @@ -0,0 +1,6 @@ + + + +

{40 + 2}

diff --git a/packages/svelte/tests/hydration/test.ts b/packages/svelte/tests/hydration/test.ts index ba13d2c611..16e4070303 100644 --- a/packages/svelte/tests/hydration/test.ts +++ b/packages/svelte/tests/hydration/test.ts @@ -53,6 +53,8 @@ const { test, run } = suite(async (config, cwd) => { await compile_directory(cwd, 'server', config.compileOptions); } + config.before_test?.(); + const target = window.document.body; const head = window.document.head; @@ -72,8 +74,6 @@ const { test, run } = suite(async (config, cwd) => { head.innerHTML = override_head ?? rendered.head; } - config.before_test?.(); - try { const snapshot = config.snapshot ? config.snapshot(target) : {}; @@ -131,19 +131,12 @@ const { test, run } = suite(async (config, cwd) => { flushSync(); - const normalize = (string: string) => - string - .trim() - .replaceAll('\r\n', '\n') - .replaceAll('/>', '>') - .replace(//g, ''); - const expected = read(`${cwd}/_expected.html`) ?? rendered.html; - assert.equal(normalize(target.innerHTML), normalize(expected)); + assert_html_equal(target.innerHTML, expected); if (rendered.head) { const expected = read(`${cwd}/_expected_head.html`) ?? rendered.head; - assert.equal(normalize(head.innerHTML), normalize(expected)); + assert_html_equal(head.innerHTML, expected); } if (config.snapshot) { diff --git a/packages/svelte/tests/migrate/samples/impossible-migrate-$derived-derived-var-3/output.svelte b/packages/svelte/tests/migrate/samples/impossible-migrate-$derived-derived-var-3/output.svelte index 26012e1115..90d2e2e036 100644 --- a/packages/svelte/tests/migrate/samples/impossible-migrate-$derived-derived-var-3/output.svelte +++ b/packages/svelte/tests/migrate/samples/impossible-migrate-$derived-derived-var-3/output.svelte @@ -4,4 +4,4 @@ - + \ No newline at end of file diff --git a/packages/svelte/tests/migrate/samples/impossible-migrate-slot-change-name/output.svelte b/packages/svelte/tests/migrate/samples/impossible-migrate-slot-change-name/output.svelte index 328966b63b..20e41646b3 100644 --- a/packages/svelte/tests/migrate/samples/impossible-migrate-slot-change-name/output.svelte +++ b/packages/svelte/tests/migrate/samples/impossible-migrate-slot-change-name/output.svelte @@ -3,4 +3,4 @@ let body; - + \ No newline at end of file diff --git a/packages/svelte/tests/migrate/samples/impossible-migrate-slot-non-identifier/output.svelte b/packages/svelte/tests/migrate/samples/impossible-migrate-slot-non-identifier/output.svelte index 1e763577df..49e9462768 100644 --- a/packages/svelte/tests/migrate/samples/impossible-migrate-slot-non-identifier/output.svelte +++ b/packages/svelte/tests/migrate/samples/impossible-migrate-slot-non-identifier/output.svelte @@ -1,2 +1,2 @@ - + \ No newline at end of file diff --git a/packages/svelte/tests/parser-legacy/samples/binding-shorthand/output.json b/packages/svelte/tests/parser-legacy/samples/binding-shorthand/output.json index 9c48025ff9..711bd54525 100644 --- a/packages/svelte/tests/parser-legacy/samples/binding-shorthand/output.json +++ b/packages/svelte/tests/parser-legacy/samples/binding-shorthand/output.json @@ -63,7 +63,7 @@ }, "end": { "line": 3, - "column": 0 + "column": 9 } }, "body": [ diff --git a/packages/svelte/tests/parser-legacy/samples/binding/output.json b/packages/svelte/tests/parser-legacy/samples/binding/output.json index 17a78f051c..b103e03683 100644 --- a/packages/svelte/tests/parser-legacy/samples/binding/output.json +++ b/packages/svelte/tests/parser-legacy/samples/binding/output.json @@ -73,7 +73,7 @@ }, "end": { "line": 3, - "column": 0 + "column": 9 } }, "body": [ diff --git a/packages/svelte/tests/parser-legacy/samples/dynamic-import/output.json b/packages/svelte/tests/parser-legacy/samples/dynamic-import/output.json index ee19d58742..76bb6c7eb6 100644 --- a/packages/svelte/tests/parser-legacy/samples/dynamic-import/output.json +++ b/packages/svelte/tests/parser-legacy/samples/dynamic-import/output.json @@ -21,7 +21,7 @@ }, "end": { "line": 9, - "column": 0 + "column": 9 } }, "body": [ diff --git a/packages/svelte/tests/parser-legacy/samples/each-block-destructured/output.json b/packages/svelte/tests/parser-legacy/samples/each-block-destructured/output.json index 637da24aea..684051e304 100644 --- a/packages/svelte/tests/parser-legacy/samples/each-block-destructured/output.json +++ b/packages/svelte/tests/parser-legacy/samples/each-block-destructured/output.json @@ -190,7 +190,7 @@ }, "end": { "line": 3, - "column": 0 + "column": 9 } }, "body": [ diff --git a/packages/svelte/tests/parser-legacy/samples/generic-snippets/output.json b/packages/svelte/tests/parser-legacy/samples/generic-snippets/output.json index 9cb5d765e8..76605f5192 100644 --- a/packages/svelte/tests/parser-legacy/samples/generic-snippets/output.json +++ b/packages/svelte/tests/parser-legacy/samples/generic-snippets/output.json @@ -258,7 +258,7 @@ }, "end": { "line": 2, - "column": 0 + "column": 9 } }, "body": [], diff --git a/packages/svelte/tests/parser-legacy/samples/javascript-comments/output.json b/packages/svelte/tests/parser-legacy/samples/javascript-comments/output.json index 4723564f17..3775c48228 100644 --- a/packages/svelte/tests/parser-legacy/samples/javascript-comments/output.json +++ b/packages/svelte/tests/parser-legacy/samples/javascript-comments/output.json @@ -298,7 +298,7 @@ }, "end": { "line": 31, - "column": 0 + "column": 9 } }, "body": [ diff --git a/packages/svelte/tests/parser-legacy/samples/refs/output.json b/packages/svelte/tests/parser-legacy/samples/refs/output.json index 7f30946fc5..1e8efe78f8 100644 --- a/packages/svelte/tests/parser-legacy/samples/refs/output.json +++ b/packages/svelte/tests/parser-legacy/samples/refs/output.json @@ -73,7 +73,7 @@ }, "end": { "line": 3, - "column": 0 + "column": 9 } }, "body": [ diff --git a/packages/svelte/tests/parser-legacy/samples/script-attribute-with-curly-braces/output.json b/packages/svelte/tests/parser-legacy/samples/script-attribute-with-curly-braces/output.json index e0b50e3b92..4ad004c88a 100644 --- a/packages/svelte/tests/parser-legacy/samples/script-attribute-with-curly-braces/output.json +++ b/packages/svelte/tests/parser-legacy/samples/script-attribute-with-curly-braces/output.json @@ -73,7 +73,7 @@ }, "end": { "line": 3, - "column": 0 + "column": 9 } }, "body": [ diff --git a/packages/svelte/tests/parser-legacy/samples/script-comment-only/output.json b/packages/svelte/tests/parser-legacy/samples/script-comment-only/output.json index b04a823e8d..0f7d0b8465 100644 --- a/packages/svelte/tests/parser-legacy/samples/script-comment-only/output.json +++ b/packages/svelte/tests/parser-legacy/samples/script-comment-only/output.json @@ -37,7 +37,7 @@ }, "end": { "line": 3, - "column": 0 + "column": 9 } }, "body": [], diff --git a/packages/svelte/tests/parser-legacy/samples/script-context-module-unquoted/output.json b/packages/svelte/tests/parser-legacy/samples/script-context-module-unquoted/output.json index 64250cb302..34a640200c 100644 --- a/packages/svelte/tests/parser-legacy/samples/script-context-module-unquoted/output.json +++ b/packages/svelte/tests/parser-legacy/samples/script-context-module-unquoted/output.json @@ -24,12 +24,12 @@ "end": 77, "loc": { "start": { - "line": 1, + "line": 5, "column": 0 }, "end": { "line": 7, - "column": 0 + "column": 9 } }, "body": [ @@ -84,7 +84,7 @@ }, "end": { "line": 3, - "column": 0 + "column": 9 } }, "body": [ diff --git a/packages/svelte/tests/parser-legacy/samples/script/output.json b/packages/svelte/tests/parser-legacy/samples/script/output.json index d3d4abd6b2..14d2805216 100644 --- a/packages/svelte/tests/parser-legacy/samples/script/output.json +++ b/packages/svelte/tests/parser-legacy/samples/script/output.json @@ -73,7 +73,7 @@ }, "end": { "line": 3, - "column": 0 + "column": 9 } }, "body": [ diff --git a/packages/svelte/tests/parser-legacy/samples/whitespace-after-script-tag/output.json b/packages/svelte/tests/parser-legacy/samples/whitespace-after-script-tag/output.json index 69b2382442..8853c32f9c 100644 --- a/packages/svelte/tests/parser-legacy/samples/whitespace-after-script-tag/output.json +++ b/packages/svelte/tests/parser-legacy/samples/whitespace-after-script-tag/output.json @@ -72,8 +72,8 @@ "column": 0 }, "end": { - "line": 3, - "column": 0 + "line": 8, + "column": 1 } }, "body": [ diff --git a/packages/svelte/tests/parser-modern/samples/comment-before-function-binding/output.json b/packages/svelte/tests/parser-modern/samples/comment-before-function-binding/output.json index 13cc5fea7b..27692e98b4 100644 --- a/packages/svelte/tests/parser-modern/samples/comment-before-function-binding/output.json +++ b/packages/svelte/tests/parser-modern/samples/comment-before-function-binding/output.json @@ -272,7 +272,7 @@ }, "end": { "line": 3, - "column": 0 + "column": 9 } }, "body": [ diff --git a/packages/svelte/tests/parser-modern/samples/comment-before-script/output.json b/packages/svelte/tests/parser-modern/samples/comment-before-script/output.json index 8d77165297..8840aad85a 100644 --- a/packages/svelte/tests/parser-modern/samples/comment-before-script/output.json +++ b/packages/svelte/tests/parser-modern/samples/comment-before-script/output.json @@ -34,12 +34,12 @@ "end": 67, "loc": { "start": { - "line": 1, + "line": 2, "column": 0 }, "end": { "line": 4, - "column": 0 + "column": 9 } }, "body": [ diff --git a/packages/svelte/tests/parser-modern/samples/generic-snippets/output.json b/packages/svelte/tests/parser-modern/samples/generic-snippets/output.json index 8e1654feed..f29eb6fc72 100644 --- a/packages/svelte/tests/parser-modern/samples/generic-snippets/output.json +++ b/packages/svelte/tests/parser-modern/samples/generic-snippets/output.json @@ -296,7 +296,7 @@ }, "end": { "line": 2, - "column": 0 + "column": 9 } }, "body": [], diff --git a/packages/svelte/tests/parser-modern/samples/loose-valid-each-as/output.json b/packages/svelte/tests/parser-modern/samples/loose-valid-each-as/output.json index b428cff0b2..441cf71519 100644 --- a/packages/svelte/tests/parser-modern/samples/loose-valid-each-as/output.json +++ b/packages/svelte/tests/parser-modern/samples/loose-valid-each-as/output.json @@ -227,7 +227,7 @@ }, "end": { "line": 3, - "column": 0 + "column": 9 } }, "body": [ diff --git a/packages/svelte/tests/parser-modern/samples/options/output.json b/packages/svelte/tests/parser-modern/samples/options/output.json index 08ce8d43b7..c220811175 100644 --- a/packages/svelte/tests/parser-modern/samples/options/output.json +++ b/packages/svelte/tests/parser-modern/samples/options/output.json @@ -111,12 +111,12 @@ "end": 93, "loc": { "start": { - "line": 1, + "line": 3, "column": 0 }, "end": { "line": 4, - "column": 0 + "column": 9 } }, "body": [], @@ -182,12 +182,12 @@ "end": 160, "loc": { "start": { - "line": 1, + "line": 6, "column": 0 }, "end": { "line": 7, - "column": 0 + "column": 9 } }, "body": [], diff --git a/packages/svelte/tests/parser-modern/samples/script-style-no-markup/output.json b/packages/svelte/tests/parser-modern/samples/script-style-no-markup/output.json index b5e42e9217..db58a62148 100644 --- a/packages/svelte/tests/parser-modern/samples/script-style-no-markup/output.json +++ b/packages/svelte/tests/parser-modern/samples/script-style-no-markup/output.json @@ -93,7 +93,7 @@ }, "end": { "line": 3, - "column": 0 + "column": 9 } }, "body": [], diff --git a/packages/svelte/tests/parser-modern/samples/snippets/output.json b/packages/svelte/tests/parser-modern/samples/snippets/output.json index c217ef94e9..f358e2a60c 100644 --- a/packages/svelte/tests/parser-modern/samples/snippets/output.json +++ b/packages/svelte/tests/parser-modern/samples/snippets/output.json @@ -230,7 +230,7 @@ }, "end": { "line": 1, - "column": 18 + "column": 27 } }, "body": [], diff --git a/packages/svelte/tests/parser-modern/samples/typescript-in-event-handler/output.json b/packages/svelte/tests/parser-modern/samples/typescript-in-event-handler/output.json index a1d6ed56bf..7b521513f4 100644 --- a/packages/svelte/tests/parser-modern/samples/typescript-in-event-handler/output.json +++ b/packages/svelte/tests/parser-modern/samples/typescript-in-event-handler/output.json @@ -396,7 +396,7 @@ }, "end": { "line": 3, - "column": 0 + "column": 9 } }, "body": [ diff --git a/packages/svelte/tests/print/samples/formatting/input.svelte b/packages/svelte/tests/print/samples/formatting/input.svelte index 9b1898e9c8..43c3b363cb 100644 --- a/packages/svelte/tests/print/samples/formatting/input.svelte +++ b/packages/svelte/tests/print/samples/formatting/input.svelte @@ -1 +1,8 @@ -

{m.hello_world({ name: 'SvelteKit User' })}

If you use VSCode, install the Sherlock i18n extensionfor a better i18n experience.

+ + +

{m.hello_world({ name: 'SvelteKit User' })}

If you use VSCode, install the Sherlock i18n extensionfor a better i18n experience.

+ +
+ + + diff --git a/packages/svelte/tests/print/samples/formatting/output.svelte b/packages/svelte/tests/print/samples/formatting/output.svelte index 7b40642580..ba871c66a5 100644 --- a/packages/svelte/tests/print/samples/formatting/output.svelte +++ b/packages/svelte/tests/print/samples/formatting/output.svelte @@ -4,18 +4,46 @@

{m.hello_world({ name: 'SvelteKit User' })}

+
+

If you use VSCode, install the - Sherlock i18n extension - + >Sherlock i18n extension + for a better i18n experience.

+ + +
+ + +
+
+ + + + + + + + + diff --git a/packages/svelte/tests/print/samples/html-document/output.svelte b/packages/svelte/tests/print/samples/html-document/output.svelte index 765f9ca84b..94ef78d0a3 100644 --- a/packages/svelte/tests/print/samples/html-document/output.svelte +++ b/packages/svelte/tests/print/samples/html-document/output.svelte @@ -1,12 +1,16 @@ + + + Svelte App +
Hello World
diff --git a/packages/svelte/tests/print/samples/style-directive/output.svelte b/packages/svelte/tests/print/samples/style-directive/output.svelte index 5aa3a6dcdb..27c12c5d47 100644 --- a/packages/svelte/tests/print/samples/style-directive/output.svelte +++ b/packages/svelte/tests/print/samples/style-directive/output.svelte @@ -1,8 +1,7 @@
...
+
- ... -
+>... diff --git a/packages/svelte/tests/print/samples/svelte-fragment/output.svelte b/packages/svelte/tests/print/samples/svelte-fragment/output.svelte index eb80023626..73a4451ed8 100644 --- a/packages/svelte/tests/print/samples/svelte-fragment/output.svelte +++ b/packages/svelte/tests/print/samples/svelte-fragment/output.svelte @@ -4,6 +4,7 @@

Hello

+

All rights reserved.

Copyright (c) 2019 Svelte Industries

diff --git a/packages/svelte/tests/print/samples/svelte-head/output.svelte b/packages/svelte/tests/print/samples/svelte-head/output.svelte index 68d352260e..da4345d731 100644 --- a/packages/svelte/tests/print/samples/svelte-head/output.svelte +++ b/packages/svelte/tests/print/samples/svelte-head/output.svelte @@ -1,5 +1,6 @@ Hello world! + Promise.resolve(); + +export default test({ + async test({ assert, target }) { + target.innerHTML = ''; + await tick(); + + const el = target.querySelector('custom-element'); + + assert.equal(el.shadowRoot, null); + } +}); diff --git a/packages/svelte/tests/runtime-browser/custom-elements-samples/closed-shadow-dom/main.svelte b/packages/svelte/tests/runtime-browser/custom-elements-samples/closed-shadow-dom/main.svelte new file mode 100644 index 0000000000..93744481b3 --- /dev/null +++ b/packages/svelte/tests/runtime-browser/custom-elements-samples/closed-shadow-dom/main.svelte @@ -0,0 +1,3 @@ + + +

Hello world!

diff --git a/packages/svelte/tests/runtime-browser/custom-elements-samples/shadow-root-init-options/_config.js b/packages/svelte/tests/runtime-browser/custom-elements-samples/shadow-root-init-options/_config.js new file mode 100644 index 0000000000..106d27929e --- /dev/null +++ b/packages/svelte/tests/runtime-browser/custom-elements-samples/shadow-root-init-options/_config.js @@ -0,0 +1,18 @@ +import { test } from '../../assert'; +const tick = () => Promise.resolve(); + +export default test({ + async test({ assert, target }) { + target.innerHTML = ''; + await tick(); + + /** @type {ShadowRoot} */ + const shadowRoot = target.querySelector('custom-element').shadowRoot; + + assert.equal(shadowRoot.mode, 'open'); + assert.equal(shadowRoot.clonable, true); + assert.equal(shadowRoot.delegatesFocus, true); + assert.equal(shadowRoot.serializable, true); + assert.equal(shadowRoot.slotAssignment, 'manual'); + } +}); diff --git a/packages/svelte/tests/runtime-browser/custom-elements-samples/shadow-root-init-options/main.svelte b/packages/svelte/tests/runtime-browser/custom-elements-samples/shadow-root-init-options/main.svelte new file mode 100644 index 0000000000..25d69d7ef9 --- /dev/null +++ b/packages/svelte/tests/runtime-browser/custom-elements-samples/shadow-root-init-options/main.svelte @@ -0,0 +1,14 @@ + + +

Hello world!

diff --git a/packages/svelte/tests/runtime-browser/samples/head-scripts/_config.js b/packages/svelte/tests/runtime-browser/samples/head-scripts/_config.js index 3ff1bf7286..4f0e53925b 100644 --- a/packages/svelte/tests/runtime-browser/samples/head-scripts/_config.js +++ b/packages/svelte/tests/runtime-browser/samples/head-scripts/_config.js @@ -4,7 +4,7 @@ export default test({ mode: ['client'], async test({ assert, window }) { // wait the script to load (maybe there a better way) - await new Promise((resolve) => setTimeout(resolve, 1)); + await new Promise((resolve) => setTimeout(resolve, 100)); assert.htmlEqual( window.document.body.innerHTML, `
123
` diff --git a/packages/svelte/tests/runtime-browser/samples/hydrate-large-text-node/_config.js b/packages/svelte/tests/runtime-browser/samples/hydrate-large-text-node/_config.js new file mode 100644 index 0000000000..3a52b1ccd3 --- /dev/null +++ b/packages/svelte/tests/runtime-browser/samples/hydrate-large-text-node/_config.js @@ -0,0 +1,24 @@ +import { test } from '../../assert'; + +// Browsers split text nodes > 65536 characters into multiple consecutive text nodes +// during HTML parsing. This test verifies that hydration correctly merges them. +const LARGE_TEXT = 'x'.repeat(70000); + +export default test({ + mode: ['hydrate'], + skip_mode: ['client'], + + props: { + text: LARGE_TEXT + }, + + async test({ assert, target }) { + const [p] = target.querySelectorAll('p'); + + // The text content should be preserved after hydration + assert.equal(p.textContent?.trim(), LARGE_TEXT); + // After hydration, there should be only one text node (plus possible comment nodes) + const textNodes = [...p.childNodes].filter((node) => node.nodeType === 3); + assert.equal(textNodes.length, 1, `Expected 1 text node, got ${textNodes.length}`); + } +}); diff --git a/packages/svelte/tests/runtime-browser/samples/hydrate-large-text-node/main.svelte b/packages/svelte/tests/runtime-browser/samples/hydrate-large-text-node/main.svelte new file mode 100644 index 0000000000..d65358dabc --- /dev/null +++ b/packages/svelte/tests/runtime-browser/samples/hydrate-large-text-node/main.svelte @@ -0,0 +1,5 @@ + + +

{text}

diff --git a/packages/svelte/tests/runtime-browser/test.ts b/packages/svelte/tests/runtime-browser/test.ts index 63e601b115..54cdc0f8be 100644 --- a/packages/svelte/tests/runtime-browser/test.ts +++ b/packages/svelte/tests/runtime-browser/test.ts @@ -164,6 +164,7 @@ async function run_test( } ], bundle: true, + platform: 'node', format: 'iife', globalName: 'test_ssr' }); @@ -196,6 +197,7 @@ async function run_test( const page = await browser.newPage(); page.on('console', (message) => { let method = message.type(); + // @ts-ignore if (method === 'warning') method = 'warn'; // @ts-ignore console[method](message.text()); @@ -211,7 +213,7 @@ async function run_test( } // uncomment to see what was generated - // fs.writeFileSync(`${test_dir}/_actual.js`, build_result.outputFiles[0].text); + // fs.writeFileSync(`${test_dir}/_output/bundle-${hydrate}.js`, build_result.outputFiles[0].text); const test_result = await page.evaluate( build_result.outputFiles[0].text + ";test.default(document.querySelector('main'))" ); diff --git a/packages/svelte/tests/runtime-legacy/samples/binding-indirect-fn/_config.js b/packages/svelte/tests/runtime-legacy/samples/binding-indirect-fn/_config.js new file mode 100644 index 0000000000..f17a7f4825 --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/binding-indirect-fn/_config.js @@ -0,0 +1,7 @@ +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + assert.htmlEqual(target.innerHTML, ``); + } +}); diff --git a/packages/svelte/tests/runtime-legacy/samples/binding-indirect-fn/main.svelte b/packages/svelte/tests/runtime-legacy/samples/binding-indirect-fn/main.svelte new file mode 100644 index 0000000000..9c1e23a0f4 --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/binding-indirect-fn/main.svelte @@ -0,0 +1,11 @@ + + +{#each items.filter(fn) as item} + +{/each} diff --git a/packages/svelte/tests/runtime-legacy/samples/dynamic-element-invalid-this-content/_config.js b/packages/svelte/tests/runtime-legacy/samples/dynamic-element-invalid-this-content/_config.js new file mode 100644 index 0000000000..b0dcaa8fdc --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/dynamic-element-invalid-this-content/_config.js @@ -0,0 +1,17 @@ +import { test } from '../../test'; + +export default test({ + mode: ['client', 'server'], + + compileOptions: { + dev: true + }, + + get props() { + return { tag: true }; + }, + + error: + 'svelte_element_invalid_this_value\n' + + 'The `this` prop on `` must be a string, if defined' +}); diff --git a/packages/svelte/tests/runtime-legacy/samples/dynamic-element-invalid-this-content/main.svelte b/packages/svelte/tests/runtime-legacy/samples/dynamic-element-invalid-this-content/main.svelte new file mode 100644 index 0000000000..cc6736a497 --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/dynamic-element-invalid-this-content/main.svelte @@ -0,0 +1,5 @@ + + +content diff --git a/packages/svelte/tests/runtime-legacy/samples/store-each-binding-logical/_config.js b/packages/svelte/tests/runtime-legacy/samples/store-each-binding-logical/_config.js new file mode 100644 index 0000000000..1134b20d0c --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/store-each-binding-logical/_config.js @@ -0,0 +1,22 @@ +import { flushSync } from 'svelte'; +import { ok, test } from '../../test'; + +export default test({ + test({ assert, target, window }) { + const input = target.querySelector('input'); + ok(input); + + const event = new window.Event('input'); + input.value = 'changed'; + input.dispatchEvent(event); + flushSync(); + + assert.htmlEqual( + target.innerHTML, + ` + +

changed

+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-legacy/samples/store-each-binding-logical/main.svelte b/packages/svelte/tests/runtime-legacy/samples/store-each-binding-logical/main.svelte new file mode 100644 index 0000000000..77731ea5a4 --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/store-each-binding-logical/main.svelte @@ -0,0 +1,13 @@ + + +{#each $items ?? [] as item} + +{/each} + +

{$items[0].text}

diff --git a/packages/svelte/tests/runtime-legacy/shared.ts b/packages/svelte/tests/runtime-legacy/shared.ts index 13975c68ee..c5317f822e 100644 --- a/packages/svelte/tests/runtime-legacy/shared.ts +++ b/packages/svelte/tests/runtime-legacy/shared.ts @@ -47,9 +47,9 @@ export interface RuntimeTest = Record; /** Temporarily skip specific modes, without skipping the entire test */ skip_mode?: Array<'server' | 'async-server' | 'client' | 'hydrate'>; - /** Skip if running with process.env.NO_ASYNC */ + /** Skip if running with process.env.SVELTE_NO_ASYNC */ skip_no_async?: boolean; - /** Skip if running without process.env.NO_ASYNC */ + /** Skip if running without process.env.SVELTE_NO_ASYNC */ skip_async?: boolean; html?: string; ssrHtml?: string; @@ -521,6 +521,8 @@ async function run_test_variant( errors, hydrate: hydrate_fn }); + + flushSync(); } if (config.runtime_error && !unhandled_rejection) { diff --git a/packages/svelte/tests/runtime-production/samples/keyed-each-unique-2/_config.js b/packages/svelte/tests/runtime-production/samples/keyed-each-unique-2/_config.js new file mode 100644 index 0000000000..bc945876ea --- /dev/null +++ b/packages/svelte/tests/runtime-production/samples/keyed-each-unique-2/_config.js @@ -0,0 +1,12 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + test({ assert, target }) { + let button = target.querySelector('button'); + + button?.click(); + + assert.throws(flushSync, 'https://svelte.dev/e/each_key_duplicate'); + } +}); diff --git a/packages/svelte/tests/runtime-production/samples/keyed-each-unique-2/main.svelte b/packages/svelte/tests/runtime-production/samples/keyed-each-unique-2/main.svelte new file mode 100644 index 0000000000..f8ba50d866 --- /dev/null +++ b/packages/svelte/tests/runtime-production/samples/keyed-each-unique-2/main.svelte @@ -0,0 +1,8 @@ + + + +{#each data as d (d)} + {d} +{/each} diff --git a/packages/svelte/tests/runtime-production/samples/keyed-each-unique-3/_config.js b/packages/svelte/tests/runtime-production/samples/keyed-each-unique-3/_config.js new file mode 100644 index 0000000000..7e1840200a --- /dev/null +++ b/packages/svelte/tests/runtime-production/samples/keyed-each-unique-3/_config.js @@ -0,0 +1,5 @@ +import { test } from '../../test'; + +export default test({ + error: 'each_key_duplicate' +}); diff --git a/packages/svelte/tests/runtime-production/samples/keyed-each-unique-3/main.svelte b/packages/svelte/tests/runtime-production/samples/keyed-each-unique-3/main.svelte new file mode 100644 index 0000000000..a05781bcb9 --- /dev/null +++ b/packages/svelte/tests/runtime-production/samples/keyed-each-unique-3/main.svelte @@ -0,0 +1,7 @@ + + +{#each data as d (d)} + {d} +{/each} diff --git a/packages/svelte/tests/runtime-production/samples/keyed-each-unique/_config.js b/packages/svelte/tests/runtime-production/samples/keyed-each-unique/_config.js new file mode 100644 index 0000000000..bc945876ea --- /dev/null +++ b/packages/svelte/tests/runtime-production/samples/keyed-each-unique/_config.js @@ -0,0 +1,12 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + test({ assert, target }) { + let button = target.querySelector('button'); + + button?.click(); + + assert.throws(flushSync, 'https://svelte.dev/e/each_key_duplicate'); + } +}); diff --git a/packages/svelte/tests/runtime-production/samples/keyed-each-unique/main.svelte b/packages/svelte/tests/runtime-production/samples/keyed-each-unique/main.svelte new file mode 100644 index 0000000000..3d52179372 --- /dev/null +++ b/packages/svelte/tests/runtime-production/samples/keyed-each-unique/main.svelte @@ -0,0 +1,8 @@ + + + +{#each data as d (d)} + {d} +{/each} diff --git a/packages/svelte/tests/runtime-production/samples/production-internals/Child.svelte b/packages/svelte/tests/runtime-production/samples/production-internals/Child.svelte new file mode 100644 index 0000000000..36b548ab8b --- /dev/null +++ b/packages/svelte/tests/runtime-production/samples/production-internals/Child.svelte @@ -0,0 +1,5 @@ + + +{foo}; diff --git a/packages/svelte/tests/runtime-production/samples/production-internals/_config.js b/packages/svelte/tests/runtime-production/samples/production-internals/_config.js new file mode 100644 index 0000000000..6ac08f7012 --- /dev/null +++ b/packages/svelte/tests/runtime-production/samples/production-internals/_config.js @@ -0,0 +1,12 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ target }) { + let button = target.querySelector('button'); + + button?.click(); + + flushSync(); + } +}); diff --git a/packages/svelte/tests/runtime-production/samples/production-internals/main.svelte b/packages/svelte/tests/runtime-production/samples/production-internals/main.svelte new file mode 100644 index 0000000000..0629644a01 --- /dev/null +++ b/packages/svelte/tests/runtime-production/samples/production-internals/main.svelte @@ -0,0 +1,19 @@ + + + + + + + diff --git a/packages/svelte/tests/runtime-production/samples/production/main.svelte b/packages/svelte/tests/runtime-production/samples/production/main.svelte new file mode 100644 index 0000000000..d9cff745d1 --- /dev/null +++ b/packages/svelte/tests/runtime-production/samples/production/main.svelte @@ -0,0 +1,7 @@ + \ No newline at end of file diff --git a/packages/svelte/tests/runtime-production/test.ts b/packages/svelte/tests/runtime-production/test.ts new file mode 100644 index 0000000000..1c9a00ab83 --- /dev/null +++ b/packages/svelte/tests/runtime-production/test.ts @@ -0,0 +1,17 @@ +// @vitest-environment jsdom + +import { vi } from 'vitest'; +import { runtime_suite, ok } from '../runtime-legacy/shared'; + +vi.mock('esm-env', async (importEnv) => { + return { + ...(await importEnv()), + DEV: false + }; +}); + +const { test, run } = runtime_suite(true); + +export { test, ok }; + +await run(__dirname); diff --git a/packages/svelte/tests/runtime-runes/samples/async-attach-hydration-mismatch/Inner.svelte b/packages/svelte/tests/runtime-runes/samples/async-attach-hydration-mismatch/Inner.svelte new file mode 100644 index 0000000000..99f885189b --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-attach-hydration-mismatch/Inner.svelte @@ -0,0 +1,4 @@ + +
{@render children?.()}
diff --git a/packages/svelte/tests/runtime-runes/samples/async-attach-hydration-mismatch/Outer.svelte b/packages/svelte/tests/runtime-runes/samples/async-attach-hydration-mismatch/Outer.svelte new file mode 100644 index 0000000000..99f885189b --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-attach-hydration-mismatch/Outer.svelte @@ -0,0 +1,4 @@ + +
{@render children?.()}
diff --git a/packages/svelte/tests/runtime-runes/samples/async-attach-hydration-mismatch/Trigger.svelte b/packages/svelte/tests/runtime-runes/samples/async-attach-hydration-mismatch/Trigger.svelte new file mode 100644 index 0000000000..fc434d748e --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-attach-hydration-mismatch/Trigger.svelte @@ -0,0 +1,7 @@ + diff --git a/packages/svelte/tests/runtime-runes/samples/async-attach-hydration-mismatch/_config.js b/packages/svelte/tests/runtime-runes/samples/async-attach-hydration-mismatch/_config.js new file mode 100644 index 0000000000..d77ba45ae4 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-attach-hydration-mismatch/_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, '
foo
'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-attach-hydration-mismatch/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-attach-hydration-mismatch/main.svelte new file mode 100644 index 0000000000..6cfc73ca25 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-attach-hydration-mismatch/main.svelte @@ -0,0 +1,16 @@ + + + + + foo + + + + diff --git a/packages/svelte/tests/runtime-runes/samples/async-await-store-mutate/_config.js b/packages/svelte/tests/runtime-runes/samples/async-await-store-mutate/_config.js new file mode 100644 index 0000000000..e89065865f --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-await-store-mutate/_config.js @@ -0,0 +1,24 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +// ensure in-place object mutations stay reactive in async +export default test({ + skip_no_async: true, + async test({ assert, target }) { + const button = /** @type {HTMLElement} */ (target.querySelector('button')); + + await tick(); + + assert.htmlEqual(target.innerHTML, `

count: 1, computed: 10

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

count: 2, computed: 20

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

count: 3, computed: 30

`); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-await-store-mutate/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-await-store-mutate/main.svelte new file mode 100644 index 0000000000..327c775d6e --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-await-store-mutate/main.svelte @@ -0,0 +1,26 @@ + + + +

count: {$data.count}, computed: {await compute($data)}

+ + {#snippet pending()} +

pending

+ {/snippet} +
+ + diff --git a/packages/svelte/tests/runtime-runes/samples/async-clear-batch-between-runs/Child.svelte b/packages/svelte/tests/runtime-runes/samples/async-clear-batch-between-runs/Child.svelte new file mode 100644 index 0000000000..d156cc99af --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-clear-batch-between-runs/Child.svelte @@ -0,0 +1,15 @@ + + +

x: {x}

+ + + {#snippet pending()} +

Loading...

+ {/snippet} + +

y: {y}

+
diff --git a/packages/svelte/tests/runtime-runes/samples/async-clear-batch-between-runs/_config.js b/packages/svelte/tests/runtime-runes/samples/async-clear-batch-between-runs/_config.js new file mode 100644 index 0000000000..fbf003f8e2 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-clear-batch-between-runs/_config.js @@ -0,0 +1,32 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: ` + + +

loading...

+ `, + + async test({ assert, target }) { + await tick(); + + const [button1, button2] = target.querySelectorAll('button'); + + button1.click(); + await tick(); + + button2.click(); + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` + + +

x: x2

+

y: y2

+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-clear-batch-between-runs/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-clear-batch-between-runs/main.svelte new file mode 100644 index 0000000000..57ab32a6ca --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-clear-batch-between-runs/main.svelte @@ -0,0 +1,19 @@ + + + + + + + + + {#snippet pending()} +

loading...

+ {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-const-wait/_config.js b/packages/svelte/tests/runtime-runes/samples/async-const-wait/_config.js new file mode 100644 index 0000000000..fe85c0393a --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-const-wait/_config.js @@ -0,0 +1,18 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + mode: ['client'], + + props: { + a_promise: Promise.resolve(10), + b_promise: Promise.resolve(20) + }, + + async test({ assert, target }) { + await tick(); + await tick(); + + assert.htmlEqual(target.innerHTML, `

30

`); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-const-wait/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-const-wait/main.svelte new file mode 100644 index 0000000000..b5e6d90f7c --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-const-wait/main.svelte @@ -0,0 +1,16 @@ + + + + {@const a = await a_promise} + {#if true} + + {@const b = await b_promise} + {#if true} + {@const sum = a + b} +

{sum}

+ {/if} +
+ {/if} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-unmount-undefined-props/Child.svelte b/packages/svelte/tests/runtime-runes/samples/async-derived-unmount-undefined-props/Child.svelte new file mode 100644 index 0000000000..fecbe222e3 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-derived-unmount-undefined-props/Child.svelte @@ -0,0 +1,9 @@ + diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-unmount-undefined-props/_config.js b/packages/svelte/tests/runtime-runes/samples/async-derived-unmount-undefined-props/_config.js new file mode 100644 index 0000000000..2f371bc6b7 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-derived-unmount-undefined-props/_config.js @@ -0,0 +1,16 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target, logs }) { + await tick(); + + assert.deepEqual(logs, ['promise resolved with:', 'some-id']); + + const button = target.querySelector('button'); + button?.click(); + await tick(); + + assert.deepEqual(logs, ['promise resolved with:', 'some-id']); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-unmount-undefined-props/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-derived-unmount-undefined-props/main.svelte new file mode 100644 index 0000000000..d5cdd5967e --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-derived-unmount-undefined-props/main.svelte @@ -0,0 +1,14 @@ + + +{#if active} + +{/if} + + diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-with-effect-and-boundary/_config.js b/packages/svelte/tests/runtime-runes/samples/async-derived-with-effect-and-boundary/_config.js new file mode 100644 index 0000000000..49428f90ad --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-derived-with-effect-and-boundary/_config.js @@ -0,0 +1,18 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + skip_no_async: true, + + async test({ assert, target }) { + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` +

baz: 69

+

+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-with-effect-and-boundary/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-derived-with-effect-and-boundary/main.svelte new file mode 100644 index 0000000000..0e05d0c414 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-derived-with-effect-and-boundary/main.svelte @@ -0,0 +1,25 @@ + + +

baz: {baz}

+ + + {#snippet pending()} +

Loading...

+ {/snippet} + + {#if qux} +

+ {/if} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-each-await-store-update/_config.js b/packages/svelte/tests/runtime-runes/samples/async-each-await-store-update/_config.js new file mode 100644 index 0000000000..165577af96 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-each-await-store-update/_config.js @@ -0,0 +1,32 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + skip_no_async: true, + async test({ assert, target }) { + const button = /** @type {HTMLElement} */ (target.querySelector('button')); + + await tick(); + + assert.htmlEqual( + target.innerHTML, + `
  • 1020
` + ); + + button.click(); + await tick(); + + assert.htmlEqual( + target.innerHTML, + `
  • 1020
  • 2040
` + ); + + button.click(); + await tick(); + + assert.htmlEqual( + target.innerHTML, + `
  • 1020
  • 2040
  • 3060
` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-each-await-store-update/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-each-await-store-update/main.svelte new file mode 100644 index 0000000000..41982b767d --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-each-await-store-update/main.svelte @@ -0,0 +1,31 @@ + + + +
    + {#each $items as item (item.id)} +
  • + {#each await query(item) as value} + {value} + {/each} +
  • + {/each} +
+ + {#snippet pending()} +

pending

+ {/snippet} +
+ + diff --git a/packages/svelte/tests/runtime-runes/samples/async-each-item-duplication/Component.svelte b/packages/svelte/tests/runtime-runes/samples/async-each-item-duplication/Component.svelte new file mode 100644 index 0000000000..9f4e638629 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-each-item-duplication/Component.svelte @@ -0,0 +1,5 @@ + + +

{message}

diff --git a/packages/svelte/tests/runtime-runes/samples/async-each-item-duplication/_config.js b/packages/svelte/tests/runtime-runes/samples/async-each-item-duplication/_config.js new file mode 100644 index 0000000000..2e20f83f7f --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-each-item-duplication/_config.js @@ -0,0 +1,14 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + mode: ['hydrate'], + + ssrHtml: `

item 1

item 2

item 3

`, + html: `

item 1

item 2

item 3

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

item 1

item 2

item 3

'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-each-item-duplication/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-each-item-duplication/main.svelte new file mode 100644 index 0000000000..ae54b63414 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-each-item-duplication/main.svelte @@ -0,0 +1,10 @@ + + +{#each messages as message} + +{/each} diff --git a/packages/svelte/tests/runtime-runes/samples/async-error-in-script/_config.js b/packages/svelte/tests/runtime-runes/samples/async-error-in-script/_config.js new file mode 100644 index 0000000000..2e0fab473c --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-error-in-script/_config.js @@ -0,0 +1,6 @@ +import { test } from '../../test'; + +export default test({ + mode: ['async-server', 'hydrate'], + error: 'oops' +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-error-in-script/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-error-in-script/main.svelte new file mode 100644 index 0000000000..64d9e6ce08 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-error-in-script/main.svelte @@ -0,0 +1,7 @@ + + +

hello

diff --git a/packages/svelte/tests/runtime-runes/samples/async-fork-derived-writable/_config.js b/packages/svelte/tests/runtime-runes/samples/async-fork-derived-writable/_config.js new file mode 100644 index 0000000000..b089b714ec --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-fork-derived-writable/_config.js @@ -0,0 +1,14 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target, logs }) { + const [btn] = target.querySelectorAll('button'); + + btn.click(); + await tick(); + // d should be 10 (real-world: s=1, d=1*10) before commit, not 20 (fork: s=2, d=2*10) + // After commit, d should be 99 (the written value) + assert.deepEqual(logs, [10, 99]); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-fork-derived-writable/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-fork-derived-writable/main.svelte new file mode 100644 index 0000000000..bc118a558a --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-fork-derived-writable/main.svelte @@ -0,0 +1,22 @@ + + + diff --git a/packages/svelte/tests/runtime-runes/samples/async-fork-derived/_config.js b/packages/svelte/tests/runtime-runes/samples/async-fork-derived/_config.js new file mode 100644 index 0000000000..59ae376167 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-fork-derived/_config.js @@ -0,0 +1,16 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target, logs }) { + const [increment] = target.querySelectorAll('button'); + + increment.click(); + await tick(); + assert.deepEqual(logs, [1, 2]); + + increment.click(); + await tick(); + assert.deepEqual(logs, [1, 2, 2, 3]); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-fork-derived/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-fork-derived/main.svelte new file mode 100644 index 0000000000..93761869d6 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-fork-derived/main.svelte @@ -0,0 +1,16 @@ + + + diff --git a/packages/svelte/tests/runtime-runes/samples/async-fork-snippet-dev/_config.js b/packages/svelte/tests/runtime-runes/samples/async-fork-snippet-dev/_config.js new file mode 100644 index 0000000000..2fb00562f5 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-fork-snippet-dev/_config.js @@ -0,0 +1,24 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + compileOptions: { + dev: true + }, + + async test({ assert, target }) { + const [fork] = target.querySelectorAll('button'); + + fork.click(); + await tick(); + + assert.htmlEqual(target.innerHTML, ''); + + const [, toggle] = target.querySelectorAll('button'); + + toggle.click(); + await tick(); + + assert.htmlEqual(target.innerHTML, ''); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-fork-snippet-dev/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-fork-snippet-dev/main.svelte new file mode 100644 index 0000000000..6945fcd153 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-fork-snippet-dev/main.svelte @@ -0,0 +1,26 @@ + + + + +{#if condition} + + + {#snippet foo({ checked })} + {checked} + {/snippet} + + +{/if} diff --git a/packages/svelte/tests/runtime-runes/samples/async-if-hydration/Child.svelte b/packages/svelte/tests/runtime-runes/samples/async-if-hydration/Child.svelte new file mode 100644 index 0000000000..02ef294d99 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-if-hydration/Child.svelte @@ -0,0 +1,5 @@ + + +{b} diff --git a/packages/svelte/tests/runtime-runes/samples/async-if-hydration/_config.js b/packages/svelte/tests/runtime-runes/samples/async-if-hydration/_config.js new file mode 100644 index 0000000000..8132e9c522 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-if-hydration/_config.js @@ -0,0 +1,11 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + mode: ['hydrate'], + + async test({ assert, target }) { + await tick(); + assert.htmlEqual(target.innerHTML, `

hello

true
`); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-if-hydration/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-if-hydration/main.svelte new file mode 100644 index 0000000000..3b08d41640 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-if-hydration/main.svelte @@ -0,0 +1,18 @@ + + +{#if a} +
+ {#if b} +

hello

+ {/if} +
+
+ +
+{/if} diff --git a/packages/svelte/tests/runtime-runes/samples/async-if-nested/_config.js b/packages/svelte/tests/runtime-runes/samples/async-if-nested/_config.js new file mode 100644 index 0000000000..49cae28cc4 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-if-nested/_config.js @@ -0,0 +1,12 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + mode: ['async-server', 'hydrate', 'client'], + ssrHtml: `bar blocking`, + + async test({ assert, target }) { + await tick(); + assert.htmlEqual(target.innerHTML, 'bar blocking'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-if-nested/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-if-nested/main.svelte new file mode 100644 index 0000000000..cfb17e3c76 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-if-nested/main.svelte @@ -0,0 +1,21 @@ + + +{#if foo} + foo +{:else if await bar} + bar +{:else} + else +{/if} + +{#if foo} + foo +{:else if !blocking} + blocking +{:else} + else +{/if} diff --git a/packages/svelte/tests/runtime-runes/samples/async-render-hydration/_config.js b/packages/svelte/tests/runtime-runes/samples/async-render-hydration/_config.js new file mode 100644 index 0000000000..2e37b0b9ac --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-render-hydration/_config.js @@ -0,0 +1,11 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + mode: ['hydrate'], + + async test({ assert, target }) { + await tick(); + assert.htmlEqual(target.innerHTML, `Count: 1 Double: 2`); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-render-hydration/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-render-hydration/main.svelte new file mode 100644 index 0000000000..e26ff87567 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-render-hydration/main.svelte @@ -0,0 +1,15 @@ + + +Count: {count} + +{#snippet ssr(num: number)} + {num} +{/snippet} + +Double: {@render ssr(double)} diff --git a/packages/svelte/tests/runtime-runes/samples/async-reschedule-during-flush/Child.svelte b/packages/svelte/tests/runtime-runes/samples/async-reschedule-during-flush/Child.svelte new file mode 100644 index 0000000000..2684005fcf --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-reschedule-during-flush/Child.svelte @@ -0,0 +1,3 @@ + diff --git a/packages/svelte/tests/runtime-runes/samples/async-reschedule-during-flush/_config.js b/packages/svelte/tests/runtime-runes/samples/async-reschedule-during-flush/_config.js new file mode 100644 index 0000000000..a837d02f9f --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-reschedule-during-flush/_config.js @@ -0,0 +1,26 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + const [a, b, resolve] = target.querySelectorAll('button'); + + a.click(); + await tick(); + + b.click(); + await tick(); + + resolve.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + + 42 + ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-reschedule-during-flush/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-reschedule-during-flush/main.svelte new file mode 100644 index 0000000000..48940017a8 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-reschedule-during-flush/main.svelte @@ -0,0 +1,23 @@ + + + + + + +{#if a} + {await push(42)} + +{/if} diff --git a/packages/svelte/tests/runtime-runes/samples/async-set-context/A.svelte b/packages/svelte/tests/runtime-runes/samples/async-set-context/A.svelte new file mode 100644 index 0000000000..5b85a553d4 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-set-context/A.svelte @@ -0,0 +1,13 @@ + + + diff --git a/packages/svelte/tests/runtime-runes/samples/async-set-context/B.svelte b/packages/svelte/tests/runtime-runes/samples/async-set-context/B.svelte new file mode 100644 index 0000000000..2c93263ca5 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-set-context/B.svelte @@ -0,0 +1,12 @@ + + + diff --git a/packages/svelte/tests/runtime-runes/samples/async-set-context/Inner.svelte b/packages/svelte/tests/runtime-runes/samples/async-set-context/C.svelte similarity index 60% rename from packages/svelte/tests/runtime-runes/samples/async-set-context/Inner.svelte rename to packages/svelte/tests/runtime-runes/samples/async-set-context/C.svelte index 2c7fd5d43d..2aebf7a64f 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-set-context/Inner.svelte +++ b/packages/svelte/tests/runtime-runes/samples/async-set-context/C.svelte @@ -2,6 +2,7 @@ import { getContext } from "svelte"; let greeting = getContext("greeting"); + let recipient = getContext("recipient"); -

{greeting}

\ No newline at end of file +

{greeting} {recipient}

diff --git a/packages/svelte/tests/runtime-runes/samples/async-set-context/Outer.svelte b/packages/svelte/tests/runtime-runes/samples/async-set-context/Outer.svelte deleted file mode 100644 index 9a493c5b75..0000000000 --- a/packages/svelte/tests/runtime-runes/samples/async-set-context/Outer.svelte +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/packages/svelte/tests/runtime-runes/samples/async-set-context/_config.js b/packages/svelte/tests/runtime-runes/samples/async-set-context/_config.js index 041f67a39e..aebed211cc 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-set-context/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-set-context/_config.js @@ -3,9 +3,9 @@ import { test } from '../../test'; export default test({ mode: ['client', 'async-server'], - ssrHtml: `

hi

`, + ssrHtml: `

hello world

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

hi

'); + assert.htmlEqual(target.innerHTML, '

hello world

'); } }); diff --git a/packages/svelte/tests/runtime-runes/samples/async-set-context/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-set-context/main.svelte index 01b46bda93..e2189e7eb8 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-set-context/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/async-set-context/main.svelte @@ -1,7 +1,7 @@ - + diff --git a/packages/svelte/tests/runtime-runes/samples/async-settled-blockers-hydration/_config.js b/packages/svelte/tests/runtime-runes/samples/async-settled-blockers-hydration/_config.js new file mode 100644 index 0000000000..f6f6c81350 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-settled-blockers-hydration/_config.js @@ -0,0 +1,14 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + skip_mode: ['server'], + skip_no_async: true, + + ssrHtml: `
1

after

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

after

'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-settled-blockers-hydration/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-settled-blockers-hydration/main.svelte new file mode 100644 index 0000000000..1b1dc17242 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-settled-blockers-hydration/main.svelte @@ -0,0 +1,13 @@ + + +{#snippet child(n)} +
{n}
+{/snippet} + +{#if n} + {@render child(n)} +{/if} + +

after

diff --git a/packages/svelte/tests/runtime-runes/samples/async-skipped-branch/_config.js b/packages/svelte/tests/runtime-runes/samples/async-skipped-branch/_config.js new file mode 100644 index 0000000000..9741b1e836 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-skipped-branch/_config.js @@ -0,0 +1,21 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + skip_no_async: true, + async test({ assert, target }) { + const [fork_btn, counter_btn] = target.querySelectorAll('button'); + + flushSync(() => { + fork_btn.click(); + }); + + assert.equal(counter_btn.textContent, '0'); + + flushSync(() => { + counter_btn.click(); + }); + + assert.equal(counter_btn.textContent, '1'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-skipped-branch/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-skipped-branch/main.svelte new file mode 100644 index 0000000000..163609ac58 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-skipped-branch/main.svelte @@ -0,0 +1,22 @@ + + + + +{#if show} + hi +{:else} + {#if show || !show} + + {/if} +{/if} + +{#if show_async} + {await new Promise(() => {})} +{/if} diff --git a/packages/svelte/tests/runtime-runes/samples/async-state-new-branch/Child.svelte b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch/Child.svelte new file mode 100644 index 0000000000..6b765526c8 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch/Child.svelte @@ -0,0 +1,7 @@ + + +{x} 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 new file mode 100644 index 0000000000..f2091eb6ab --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch/_config.js @@ -0,0 +1,47 @@ +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'); + + x.click(); + await tick(); + assert.deepEqual(logs, ['universe']); + + y.click(); + await tick(); + assert.deepEqual(logs, ['universe', 'world', '$effect: world']); + assert.htmlEqual( + target.innerHTML, + ` + + + + world + ` + ); + + resolve.click(); + await tick(); + assert.deepEqual(logs, [ + 'universe', + 'world', + '$effect: world', + '$effect: universe', + '$effect: universe' + ]); + assert.htmlEqual( + target.innerHTML, + ` + + + + universe + universe + universe + ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-state-new-branch/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch/main.svelte new file mode 100644 index 0000000000..8edc718de2 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch/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-static-derived-after-await/_config.js b/packages/svelte/tests/runtime-runes/samples/async-static-derived-after-await/_config.js new file mode 100644 index 0000000000..5c95b3149c --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-static-derived-after-await/_config.js @@ -0,0 +1,9 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + await tick(); + assert.htmlEqual(target.innerHTML, `

hello

`); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-static-derived-after-await/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-static-derived-after-await/main.svelte new file mode 100644 index 0000000000..9ecc839c3c --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-static-derived-after-await/main.svelte @@ -0,0 +1,6 @@ + + +

{message}

diff --git a/packages/svelte/tests/runtime-runes/samples/async-transform-empty-statements/_config.js b/packages/svelte/tests/runtime-runes/samples/async-transform-empty-statements/_config.js new file mode 100644 index 0000000000..2e4a27cf09 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-transform-empty-statements/_config.js @@ -0,0 +1,5 @@ +import { test } from '../../test'; + +export default test({ + async test() {} +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-transform-empty-statements/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-transform-empty-statements/main.svelte new file mode 100644 index 0000000000..03c4ada55a --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-transform-empty-statements/main.svelte @@ -0,0 +1,6 @@ + + +{name} \ No newline at end of file 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 new file mode 100644 index 0000000000..e7983a3de9 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/await-html-hydration/_config.js @@ -0,0 +1,7 @@ +import { test } from '../../test'; + +export default test({ + skip_no_async: true, + mode: ['hydrate'], + async test() {} +}); diff --git a/packages/svelte/tests/runtime-runes/samples/await-html-hydration/main.svelte b/packages/svelte/tests/runtime-runes/samples/await-html-hydration/main.svelte new file mode 100644 index 0000000000..3b9ce94e61 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/await-html-hydration/main.svelte @@ -0,0 +1 @@ +
{@html await Promise.resolve(`Foo`)}
\ No newline at end of file diff --git a/packages/svelte/tests/runtime-runes/samples/create-context-programmatic/Child.svelte b/packages/svelte/tests/runtime-runes/samples/create-context-programmatic/Child.svelte new file mode 100644 index 0000000000..3e39d5043e --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/create-context-programmatic/Child.svelte @@ -0,0 +1,7 @@ + + +

{message}

diff --git a/packages/svelte/tests/runtime-runes/samples/create-context-programmatic/_config.js b/packages/svelte/tests/runtime-runes/samples/create-context-programmatic/_config.js new file mode 100644 index 0000000000..f4374c8759 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/create-context-programmatic/_config.js @@ -0,0 +1,8 @@ +import { test } from '../../test'; + +export default test({ + ssrHtml: `
`, + html: `

hello

`, + + test() {} +}); diff --git a/packages/svelte/tests/runtime-runes/samples/create-context-programmatic/main.svelte b/packages/svelte/tests/runtime-runes/samples/create-context-programmatic/main.svelte new file mode 100644 index 0000000000..3c42dbf180 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/create-context-programmatic/main.svelte @@ -0,0 +1,20 @@ + + +
{ + mount(Wrapper(Child), { target }); +}}>
diff --git a/packages/svelte/tests/runtime-runes/samples/derived-read-outside-reaction/_config.js b/packages/svelte/tests/runtime-runes/samples/derived-read-outside-reaction/_config.js new file mode 100644 index 0000000000..f3a71b93e6 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/derived-read-outside-reaction/_config.js @@ -0,0 +1,47 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: ` + + +

1, 2, 3

+ `, + + test({ assert, target }) { + const [button1, button2] = target.querySelectorAll('button'); + + button1.click(); + flushSync(); + assert.htmlEqual( + target.innerHTML, + ` + + +

2, 4, 6

+ ` + ); + + button2.click(); + flushSync(); + assert.htmlEqual( + target.innerHTML, + ` + + +

2, 4, 6, 8

+ ` + ); + + button1.click(); + flushSync(); + assert.htmlEqual( + target.innerHTML, + ` + + +

3, 6, 9, 12

+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/derived-read-outside-reaction/main.svelte b/packages/svelte/tests/runtime-runes/samples/derived-read-outside-reaction/main.svelte new file mode 100644 index 0000000000..0b5096dfa4 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/derived-read-outside-reaction/main.svelte @@ -0,0 +1,30 @@ + + + + + + +

{products.join(', ')}

diff --git a/packages/svelte/tests/runtime-runes/samples/each-non-branch-effects/_config.js b/packages/svelte/tests/runtime-runes/samples/each-non-branch-effects/_config.js new file mode 100644 index 0000000000..acf84f6809 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/each-non-branch-effects/_config.js @@ -0,0 +1,43 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + const addBtn = /** @type {HTMLElement} */ (target.querySelector('button.add')); + const removeBtn = /** @type {HTMLElement} */ (target.querySelector('button.remove')); + + const btnHtml = ''; + + assert.htmlEqual(target.innerHTML, btnHtml); + + addBtn.click(); + flushSync(); + + assert.htmlEqual(target.innerHTML, `1${btnHtml}`); + + addBtn.click(); + flushSync(); + + assert.htmlEqual(target.innerHTML, `12${btnHtml}`); + + addBtn.click(); + flushSync(); + + assert.htmlEqual(target.innerHTML, `123${btnHtml}`); + + removeBtn.click(); + flushSync(); + + assert.htmlEqual(target.innerHTML, `12${btnHtml}`); + + removeBtn.click(); + flushSync(); + + assert.htmlEqual(target.innerHTML, `1${btnHtml}`); + + addBtn.click(); + flushSync(); + + assert.htmlEqual(target.innerHTML, `12${btnHtml}`); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/each-non-branch-effects/main.svelte b/packages/svelte/tests/runtime-runes/samples/each-non-branch-effects/main.svelte new file mode 100644 index 0000000000..7869d095dd --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/each-non-branch-effects/main.svelte @@ -0,0 +1,30 @@ + + +{#each proxy as item} + {item} +{/each} + + + diff --git a/packages/svelte/tests/runtime-runes/samples/effect-loop-infinite/_config.js b/packages/svelte/tests/runtime-runes/samples/effect-loop-infinite/_config.js index 57f60c2b44..44cf5b09e2 100644 --- a/packages/svelte/tests/runtime-runes/samples/effect-loop-infinite/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/effect-loop-infinite/_config.js @@ -11,11 +11,12 @@ export default test({ test({ assert, errors }) { const [button] = document.querySelectorAll('button'); - try { + assert.throws(() => { flushSync(() => button.click()); - } catch (e) { - assert.equal(errors.length, 1); // for whatever reason we can't get the name which should be 'updated at' - assert.ok(/** @type {Error} */ (e).message.startsWith('effect_update_depth_exceeded')); - } + }, /effect_update_depth_exceeded/); + + assert.equal(errors.length, 1); + + assert.doesNotThrow(flushSync); } }); diff --git a/packages/svelte/tests/runtime-runes/samples/error-boundary-12/main.svelte b/packages/svelte/tests/runtime-runes/samples/error-boundary-12/main.svelte index d9dee1e2b0..748acab91e 100644 --- a/packages/svelte/tests/runtime-runes/samples/error-boundary-12/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/error-boundary-12/main.svelte @@ -15,6 +15,6 @@ {d} {#snippet failed()} -

Error occured

+

Error occurred

{/snippet} diff --git a/packages/svelte/tests/runtime-runes/samples/error-boundary-24/Child.svelte b/packages/svelte/tests/runtime-runes/samples/error-boundary-24/Child.svelte new file mode 100644 index 0000000000..cd25ece1b5 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/error-boundary-24/Child.svelte @@ -0,0 +1,5 @@ + + +

Child content

diff --git a/packages/svelte/tests/runtime-runes/samples/error-boundary-24/_config.js b/packages/svelte/tests/runtime-runes/samples/error-boundary-24/_config.js new file mode 100644 index 0000000000..b5b5f296d8 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/error-boundary-24/_config.js @@ -0,0 +1,13 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + mode: ['client'], + test({ assert, target }) { + flushSync(); + + // When exception is set by onerror, the {#if !exception} block should hide + // and only the {#if exception} block should be visible + assert.htmlEqual(target.innerHTML, '

caught error: child error

'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/error-boundary-24/main.svelte b/packages/svelte/tests/runtime-runes/samples/error-boundary-24/main.svelte new file mode 100644 index 0000000000..5116580003 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/error-boundary-24/main.svelte @@ -0,0 +1,19 @@ + + +{#if !exception} +

condition is {String(!exception)}

+ + + +{/if} + +{#if exception} +

caught error: {exception.message}

+{/if} diff --git a/packages/svelte/tests/runtime-runes/samples/error-boundary-25/Child.svelte b/packages/svelte/tests/runtime-runes/samples/error-boundary-25/Child.svelte new file mode 100644 index 0000000000..3d5770fc61 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/error-boundary-25/Child.svelte @@ -0,0 +1,8 @@ + + +

+ boom +

diff --git a/packages/svelte/tests/runtime-runes/samples/error-boundary-25/_config.js b/packages/svelte/tests/runtime-runes/samples/error-boundary-25/_config.js new file mode 100644 index 0000000000..b5b5f296d8 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/error-boundary-25/_config.js @@ -0,0 +1,13 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + mode: ['client'], + test({ assert, target }) { + flushSync(); + + // When exception is set by onerror, the {#if !exception} block should hide + // and only the {#if exception} block should be visible + assert.htmlEqual(target.innerHTML, '

caught error: child error

'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/error-boundary-25/main.svelte b/packages/svelte/tests/runtime-runes/samples/error-boundary-25/main.svelte new file mode 100644 index 0000000000..243d10cf3a --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/error-boundary-25/main.svelte @@ -0,0 +1,19 @@ + + +{#if !exception} +

condition is {String(!exception)}

+ + + +{/if} + +{#if exception} +

caught error: {exception}

+{/if} diff --git a/packages/svelte/tests/runtime-runes/samples/fork-derived-class-instance/_config.js b/packages/svelte/tests/runtime-runes/samples/fork-derived-class-instance/_config.js new file mode 100644 index 0000000000..feac447536 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/fork-derived-class-instance/_config.js @@ -0,0 +1,22 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + skip_no_async: true, + async test({ assert, target }) { + const [fork] = target.querySelectorAll('button'); + + fork.click(); + await tick(); + + const [, increment] = target.querySelectorAll('button'); + const p = target.querySelector('p'); + + assert.equal(p?.textContent, '0'); + + increment.click(); + await tick(); + + assert.equal(p?.textContent, '1'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/fork-derived-class-instance/main.svelte b/packages/svelte/tests/runtime-runes/samples/fork-derived-class-instance/main.svelte new file mode 100644 index 0000000000..3623c1df66 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/fork-derived-class-instance/main.svelte @@ -0,0 +1,23 @@ + + + + +{#if condition} + +

{counter.count}

+{/if} diff --git a/packages/svelte/tests/runtime-runes/samples/fork-derived-dependency-rollback/_config.js b/packages/svelte/tests/runtime-runes/samples/fork-derived-dependency-rollback/_config.js new file mode 100644 index 0000000000..c5db69f7d4 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/fork-derived-dependency-rollback/_config.js @@ -0,0 +1,25 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + skip_no_async: true, + async test({ assert, target }) { + const [fork_btn, _toggle_btn, inc_count_1_btn] = target.querySelectorAll('button'); + const p = /** @type {HTMLElement} */ (target.querySelector('p')); + + assert.equal(p.textContent, '0'); + + // Trigger derived to re-evaluate during fork and switch to tracking count_2 + flushSync(() => { + fork_btn.click(); + }); + + assert.equal(p.textContent, '0'); + + flushSync(() => { + inc_count_1_btn.click(); + }); + + assert.equal(p.textContent, '1'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/fork-derived-dependency-rollback/main.svelte b/packages/svelte/tests/runtime-runes/samples/fork-derived-dependency-rollback/main.svelte new file mode 100644 index 0000000000..800c82b56b --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/fork-derived-dependency-rollback/main.svelte @@ -0,0 +1,27 @@ + + + +{#if count} +{/if} + + + + + + + +

{count}

diff --git a/packages/svelte/tests/runtime-runes/samples/fork-derived-uncached-1/_config.js b/packages/svelte/tests/runtime-runes/samples/fork-derived-uncached-1/_config.js new file mode 100644 index 0000000000..fb50ceb9b1 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/fork-derived-uncached-1/_config.js @@ -0,0 +1,40 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + skip_no_async: true, + async test({ assert, target }) { + const [fork, toggle, increment] = target.querySelectorAll('button'); + + // derived is first evaluated in block effect, then discarded + flushSync(() => fork.click()); + + // should not throw "Cannot convert a Symbol value to a string" due to cached UNINITIALIZED from first fork + flushSync(() => fork.click()); + + // should not reflect the temporary change to `clicks` inside the fork + flushSync(() => toggle.click()); + + assert.htmlEqual( + target.innerHTML, + ` + + + +

0

+ ` + ); + + flushSync(() => increment.click()); + + assert.htmlEqual( + target.innerHTML, + ` + + + +

2

+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/fork-derived-uncached-1/main.svelte b/packages/svelte/tests/runtime-runes/samples/fork-derived-uncached-1/main.svelte new file mode 100644 index 0000000000..08ebaba25a --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/fork-derived-uncached-1/main.svelte @@ -0,0 +1,23 @@ + + + + + + + + +{#if show} +

{derived}

+{/if} + diff --git a/packages/svelte/tests/runtime-runes/samples/fork-derived-uncached-2/_config.js b/packages/svelte/tests/runtime-runes/samples/fork-derived-uncached-2/_config.js new file mode 100644 index 0000000000..21ab140fc0 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/fork-derived-uncached-2/_config.js @@ -0,0 +1,38 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + skip_no_async: true, + async test({ assert, target }) { + const [fork, toggle, increment] = target.querySelectorAll('button'); + + // derived is first evaluated in block effect, then discarded + flushSync(() => fork.click()); + + // should not reflect the temporary change to `clicks` inside the fork + // or throw "Cannot convert a Symbol value to a string" due to cached UNINITIALIZED + flushSync(() => toggle.click()); + + assert.htmlEqual( + target.innerHTML, + ` + + + +

0

+ ` + ); + + flushSync(() => increment.click()); + + assert.htmlEqual( + target.innerHTML, + ` + + + +

2

+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/fork-derived-uncached-2/main.svelte b/packages/svelte/tests/runtime-runes/samples/fork-derived-uncached-2/main.svelte new file mode 100644 index 0000000000..08ebaba25a --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/fork-derived-uncached-2/main.svelte @@ -0,0 +1,23 @@ + + + + + + + + +{#if show} +

{derived}

+{/if} + diff --git a/packages/svelte/tests/runtime-runes/samples/fork-derived-uncached-3/_config.js b/packages/svelte/tests/runtime-runes/samples/fork-derived-uncached-3/_config.js new file mode 100644 index 0000000000..4827aa99f3 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/fork-derived-uncached-3/_config.js @@ -0,0 +1,32 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + skip_no_async: true, + async test({ assert, target }) { + const [fork, toggle, increment] = target.querySelectorAll('button'); + + // initialize derived by showing it + flushSync(() => toggle.click()); + flushSync(() => toggle.click()); + + // increment clicks + flushSync(() => increment.click()); + + // update derived, but without writing to `derived.v` + flushSync(() => fork.click()); + + // show derived + flushSync(() => toggle.click()); + + assert.htmlEqual( + target.innerHTML, + ` + + + +

2

+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/fork-derived-uncached-3/main.svelte b/packages/svelte/tests/runtime-runes/samples/fork-derived-uncached-3/main.svelte new file mode 100644 index 0000000000..08ebaba25a --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/fork-derived-uncached-3/main.svelte @@ -0,0 +1,23 @@ + + + + + + + + +{#if show} +

{derived}

+{/if} + diff --git a/packages/svelte/tests/runtime-runes/samples/hydratable-script-escape/_config.js b/packages/svelte/tests/runtime-runes/samples/hydratable-script-escape/_config.js new file mode 100644 index 0000000000..bc51795565 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/hydratable-script-escape/_config.js @@ -0,0 +1,14 @@ +import { test } from '../../test'; + +export default test({ + skip_no_async: true, + mode: ['hydrate'], + + props: { + key: '' + }, + + async test() { + // this test will fail when evaluating the `head` script if the vulnerability is present + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/hydratable-script-escape/main.svelte b/packages/svelte/tests/runtime-runes/samples/hydratable-script-escape/main.svelte new file mode 100644 index 0000000000..1327492190 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/hydratable-script-escape/main.svelte @@ -0,0 +1,9 @@ + + +

{value}

diff --git a/packages/svelte/tests/runtime-runes/samples/key-nan/_config.js b/packages/svelte/tests/runtime-runes/samples/key-nan/_config.js new file mode 100644 index 0000000000..bd0dd06761 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/key-nan/_config.js @@ -0,0 +1,17 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: '

it rendered

', + + test({ assert, target, logs }) { + assert.deepEqual(logs, ['rendering']); + + const btn = target.querySelector('button'); + flushSync(() => btn?.click()); + + // should not re-render + assert.deepEqual(logs, ['rendering']); + assert.htmlEqual(target.innerHTML, '

it rendered

'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/key-nan/main.svelte b/packages/svelte/tests/runtime-runes/samples/key-nan/main.svelte new file mode 100644 index 0000000000..24f7f2d179 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/key-nan/main.svelte @@ -0,0 +1,10 @@ + + + + +{#key x} + {console.log('rendering')} +

it rendered

+{/key} diff --git a/packages/svelte/tests/runtime-runes/samples/optgroup-rich-content/_config.js b/packages/svelte/tests/runtime-runes/samples/optgroup-rich-content/_config.js new file mode 100644 index 0000000000..9de60bcb56 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/optgroup-rich-content/_config.js @@ -0,0 +1,31 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +// Test that optgroup with rich HTML content (non-option elements) and dynamic expressions works correctly +export default test({ + mode: ['client', 'hydrate'], + test({ assert, target }) { + const select = /** @type {HTMLSelectElement} */ (target.querySelector('select')); + const optgroups = target.querySelectorAll('optgroup'); + const options = target.querySelectorAll('option'); + const button = /** @type {HTMLButtonElement} */ (target.querySelector('button')); + + assert.ok(select); + assert.equal(optgroups.length, 2); + assert.equal(options.length, 4); + + // Check initial option content (rich content inside optgroup) + assert.equal(options[0]?.textContent, 'apple apple'); + assert.equal(options[1]?.textContent, 'banana'); + assert.equal(options[2]?.textContent, 'carrot carrot'); + assert.equal(options[3]?.textContent, 'Plain celery'); + + // Click button to change dynamic content + button.click(); + flushSync(); + + // Check updated option content + assert.equal(options[0]?.textContent, 'orange orange'); + assert.equal(options[2]?.textContent, 'broccoli broccoli'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/optgroup-rich-content/main.svelte b/packages/svelte/tests/runtime-runes/samples/optgroup-rich-content/main.svelte new file mode 100644 index 0000000000..03777a1cfb --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/optgroup-rich-content/main.svelte @@ -0,0 +1,19 @@ + + + + + diff --git a/packages/svelte/tests/runtime-runes/samples/option-rich-content/_config.js b/packages/svelte/tests/runtime-runes/samples/option-rich-content/_config.js new file mode 100644 index 0000000000..3c9e5847cd --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/option-rich-content/_config.js @@ -0,0 +1,37 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +// Test that rich HTML content in
Home Away

${$.escape(title)}

we don't need to traverse these nodes

or

these

ones

${$.html(content)}

these

trailing

nodes

can

be

completely

ignored

+ + + + + + + diff --git a/packages/svelte/tests/validator/samples/a11y-no-noninteractive-element-to-interactive-role/input.svelte b/packages/svelte/tests/validator/samples/a11y-no-noninteractive-element-to-interactive-role/input.svelte index e5db8719b0..edfa3eeada 100644 --- a/packages/svelte/tests/validator/samples/a11y-no-noninteractive-element-to-interactive-role/input.svelte +++ b/packages/svelte/tests/validator/samples/a11y-no-noninteractive-element-to-interactive-role/input.svelte @@ -69,6 +69,13 @@
  • + + + + + + +
    diff --git a/packages/svelte/tests/validator/samples/a11y-no-redundant-roles/input.svelte b/packages/svelte/tests/validator/samples/a11y-no-redundant-roles/input.svelte index 2ccd765aa2..9e06ebe420 100644 --- a/packages/svelte/tests/validator/samples/a11y-no-redundant-roles/input.svelte +++ b/packages/svelte/tests/validator/samples/a11y-no-redundant-roles/input.svelte @@ -22,7 +22,7 @@
    - +
    @@ -44,5 +44,5 @@
    - + diff --git a/packages/svelte/tests/validator/samples/a11y-no-redundant-roles/warnings.json b/packages/svelte/tests/validator/samples/a11y-no-redundant-roles/warnings.json index 861f000aef..e041037d71 100644 --- a/packages/svelte/tests/validator/samples/a11y-no-redundant-roles/warnings.json +++ b/packages/svelte/tests/validator/samples/a11y-no-redundant-roles/warnings.json @@ -275,18 +275,6 @@ "line": 24 } }, - { - "code": "a11y_no_redundant_roles", - "end": { - "column": 17, - "line": 25 - }, - "message": "Redundant role 'list'", - "start": { - "column": 6, - "line": 25 - } - }, { "code": "a11y_no_redundant_roles", "end": { diff --git a/packages/svelte/tests/validator/samples/a11y-no-static-element-interactions/input.svelte b/packages/svelte/tests/validator/samples/a11y-no-static-element-interactions/input.svelte index ee7ed4f084..8829d784c8 100644 --- a/packages/svelte/tests/validator/samples/a11y-no-static-element-interactions/input.svelte +++ b/packages/svelte/tests/validator/samples/a11y-no-static-element-interactions/input.svelte @@ -17,3 +17,6 @@
    {}}>
    {}} on:mouseup={() => {}} on:copy={() => {}}>link +
    {}}>
    +
    {}}>
    +
    {}}>
    diff --git a/packages/svelte/tests/validator/samples/a11y-no-static-element-interactions/warnings.json b/packages/svelte/tests/validator/samples/a11y-no-static-element-interactions/warnings.json index ab4b58a839..41325a8756 100644 --- a/packages/svelte/tests/validator/samples/a11y-no-static-element-interactions/warnings.json +++ b/packages/svelte/tests/validator/samples/a11y-no-static-element-interactions/warnings.json @@ -22,5 +22,41 @@ "column": 0, "line": 19 } + }, + { + "code": "a11y_no_static_element_interactions", + "end": { + "column": 37, + "line": 20 + }, + "message": "`
    ` with a pointerdown handler must have an ARIA role", + "start": { + "column": 0, + "line": 20 + } + }, + { + "code": "a11y_no_static_element_interactions", + "end": { + "column": 38, + "line": 21 + }, + "message": "`
    ` with a pointerenter handler must have an ARIA role", + "start": { + "column": 0, + "line": 21 + } + }, + { + "code": "a11y_no_static_element_interactions", + "end": { + "column": 36, + "line": 22 + }, + "message": "`
    ` with a touchstart handler must have an ARIA role", + "start": { + "column": 0, + "line": 22 + } } ] diff --git a/packages/svelte/tests/validator/samples/a11y-popover-label/input.svelte b/packages/svelte/tests/validator/samples/a11y-popover-label/input.svelte new file mode 100644 index 0000000000..6fafa0a10c --- /dev/null +++ b/packages/svelte/tests/validator/samples/a11y-popover-label/input.svelte @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/packages/svelte/tests/validator/samples/a11y-popover-label/warnings.json b/packages/svelte/tests/validator/samples/a11y-popover-label/warnings.json new file mode 100644 index 0000000000..f5fd09c89d --- /dev/null +++ b/packages/svelte/tests/validator/samples/a11y-popover-label/warnings.json @@ -0,0 +1,14 @@ +[ + { + "code": "a11y_consider_explicit_label", + "message": "Buttons and links should either contain text or have an `aria-label`, `aria-labelledby` or `title` attribute", + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 3, + "column": 9 + } + } +] diff --git a/packages/svelte/tests/validator/samples/const-tag-placement-1/errors.json b/packages/svelte/tests/validator/samples/const-tag-placement-1/errors.json index 514e5d0561..f7f5803e74 100644 --- a/packages/svelte/tests/validator/samples/const-tag-placement-1/errors.json +++ b/packages/svelte/tests/validator/samples/const-tag-placement-1/errors.json @@ -1,7 +1,7 @@ [ { "code": "const_tag_invalid_placement", - "message": "`{@const}` must be the immediate child of `{#snippet}`, `{#if}`, `{:else if}`, `{:else}`, `{#each}`, `{:then}`, `{:catch}`, ``, ``", + "message": "`{@const}` must be the immediate child of `{#snippet}`, `{#if}`, `{:else if}`, `{:else}`, `{#each}`, `{:then}`, `{:catch}`, ``, `` or ``", "start": { "line": 5, "column": 0 diff --git a/packages/svelte/tests/validator/samples/const-tag-placement-2/errors.json b/packages/svelte/tests/validator/samples/const-tag-placement-2/errors.json index 6b968f7eda..d008c6ce4c 100644 --- a/packages/svelte/tests/validator/samples/const-tag-placement-2/errors.json +++ b/packages/svelte/tests/validator/samples/const-tag-placement-2/errors.json @@ -1,7 +1,7 @@ [ { "code": "const_tag_invalid_placement", - "message": "`{@const}` must be the immediate child of `{#snippet}`, `{#if}`, `{:else if}`, `{:else}`, `{#each}`, `{:then}`, `{:catch}`, ``, ``", + "message": "`{@const}` must be the immediate child of `{#snippet}`, `{#if}`, `{:else if}`, `{:else}`, `{#each}`, `{:then}`, `{:catch}`, ``, `` or ``", "start": { "line": 7, "column": 4 diff --git a/packages/svelte/tests/validator/samples/tag-non-string/errors.json b/packages/svelte/tests/validator/samples/tag-non-string/errors.json index 71f8df4d00..5473e18b7d 100644 --- a/packages/svelte/tests/validator/samples/tag-non-string/errors.json +++ b/packages/svelte/tests/validator/samples/tag-non-string/errors.json @@ -1,7 +1,7 @@ [ { "code": "svelte_options_invalid_customelement", - "message": "\"customElement\" must be a string literal defining a valid custom element name or an object of the form { tag?: string; shadow?: \"open\" | \"none\"; props?: { [key: string]: { attribute?: string; reflect?: boolean; type: .. } } }", + "message": "\"customElement\" must be a string literal defining a valid custom element name or an object of the form { tag?: string; shadow?: \"open\" | \"none\" | `ShadowRootInit`; props?: { [key: string]: { attribute?: string; reflect?: boolean; type: .. } } }", "start": { "line": 1, "column": 16 diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index 53b9a3d1ae..bdbc459c4a 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -888,6 +888,12 @@ declare module 'svelte/compiler' { modern?: false; loose?: boolean; } | undefined): Record; + /** + * The parseCss function parses a CSS stylesheet, returning its abstract syntax tree. + * + * @param source The CSS source code + * */ + export function parseCss(source: string): Omit; /** * @deprecated Replace this with `import { walk } from 'estree-walker'` * */ @@ -1232,7 +1238,7 @@ declare module 'svelte/compiler' { css?: 'injected'; customElement?: { tag?: string; - shadow?: 'open' | 'none'; + shadow?: 'open' | 'none' | ObjectExpression | undefined; props?: Record< string, { @@ -2557,6 +2563,7 @@ declare module 'svelte/server' { props?: NoInfer; context?: Map; idPrefix?: string; + csp?: Csp; } ] : [ @@ -2565,9 +2572,14 @@ declare module 'svelte/server' { props: NoInfer; context?: Map; idPrefix?: string; + csp?: Csp; } ] ): RenderOutput; + type Csp = { nonce?: string; hash?: boolean }; + + type Sha256Source = `sha256-${string}`; + interface SyncRenderOutput { /** HTML that goes into the `` */ head: string; @@ -2575,6 +2587,9 @@ declare module 'svelte/server' { html: string; /** HTML that goes somewhere into the `` */ body: string; + hashes: { + script: Sha256Source[]; + }; } type RenderOutput = SyncRenderOutput & PromiseLike; @@ -2805,7 +2820,7 @@ declare module 'svelte/events' { export function on( window: Window, type: Type, - handler: (this: Window, event: WindowEventMap[Type]) => any, + handler: (this: Window, event: WindowEventMap[Type] & { currentTarget: Window }) => any, options?: AddEventListenerOptions | undefined ): () => void; /** @@ -2816,7 +2831,7 @@ declare module 'svelte/events' { export function on( document: Document, type: Type, - handler: (this: Document, event: DocumentEventMap[Type]) => any, + handler: (this: Document, event: DocumentEventMap[Type] & { currentTarget: Document }) => any, options?: AddEventListenerOptions | undefined ): () => void; /** @@ -2827,7 +2842,7 @@ declare module 'svelte/events' { export function on( element: Element, type: Type, - handler: (this: Element, event: HTMLElementEventMap[Type]) => any, + handler: (this: Element, event: HTMLElementEventMap[Type] & { currentTarget: Element }) => any, options?: AddEventListenerOptions | undefined ): () => void; /** @@ -2838,7 +2853,7 @@ declare module 'svelte/events' { export function on( element: Element, type: Type, - handler: (this: Element, event: MediaQueryListEventMap[Type]) => any, + handler: (this: Element, event: MediaQueryListEventMap[Type] & { currentTarget: Element }) => any, options?: AddEventListenerOptions | undefined ): () => void; /** @@ -3177,7 +3192,7 @@ declare module 'svelte/types/compiler/interfaces' { * let count = $state(0); * ``` * - * https://svelte.dev/docs/svelte/$state + * @see {@link https://svelte.dev/docs/svelte/$state Documentation} * * @param initial The initial value */ @@ -3287,7 +3302,7 @@ declare namespace $state { * * ``` * - * https://svelte.dev/docs/svelte/$state#$state.raw + * @see {@link https://svelte.dev/docs/svelte/$state#$state.raw Documentation} * * @param initial The initial value */ @@ -3308,7 +3323,7 @@ declare namespace $state { * * ``` * - * https://svelte.dev/docs/svelte/$state#$state.snapshot + * @see {@link https://svelte.dev/docs/svelte/$state#$state.snapshot Documentation} * * @param state The value to snapshot */ @@ -3334,6 +3349,9 @@ declare namespace $state { export const prototype: never; /** @deprecated */ export const toString: never; + + // needed to keep private stuff private + export {}; } /** @@ -3345,7 +3363,7 @@ declare namespace $state { * let double = $derived(count * 2); * ``` * - * https://svelte.dev/docs/svelte/$derived + * @see {@link https://svelte.dev/docs/svelte/$derived Documentation} * * @param expression The derived state expression */ @@ -3367,7 +3385,7 @@ declare namespace $derived { * }); * ``` * - * https://svelte.dev/docs/svelte/$derived#$derived.by + * @see {@link https://svelte.dev/docs/svelte/$derived#$derived.by Documentation} */ export function by(fn: () => T): T; @@ -3391,6 +3409,9 @@ declare namespace $derived { export const prototype: never; /** @deprecated */ export const toString: never; + + // needed to keep private stuff private + export {}; } /** @@ -3406,7 +3427,7 @@ declare namespace $derived { * * Does not run during server-side rendering. * - * https://svelte.dev/docs/svelte/$effect + * @see {@link https://svelte.dev/docs/svelte/$effect Documentation} * @param fn The function to execute */ declare function $effect(fn: () => void | (() => void)): void; @@ -3425,7 +3446,7 @@ declare namespace $effect { * * Does not run during server-side rendering. * - * https://svelte.dev/docs/svelte/$effect#$effect.pre + * @see {@link https://svelte.dev/docs/svelte/$effect#$effect.pre Documentation} * @param fn The function to execute */ export function pre(fn: () => void | (() => void)): void; @@ -3433,7 +3454,7 @@ declare namespace $effect { /** * Returns the number of promises that are pending in the current boundary, not including child boundaries. * - * https://svelte.dev/docs/svelte/$effect#$effect.pending + * @see {@link https://svelte.dev/docs/svelte/$effect#$effect.pending Documentation} */ export function pending(): number; @@ -3455,7 +3476,7 @@ declare namespace $effect { * * This allows you to (for example) add things like subscriptions without causing memory leaks, by putting them in child effects. * - * https://svelte.dev/docs/svelte/$effect#$effect.tracking + * @see {@link https://svelte.dev/docs/svelte/$effect#$effect.tracking Documentation} */ export function tracking(): boolean; @@ -3483,7 +3504,7 @@ declare namespace $effect { * * ``` * - * https://svelte.dev/docs/svelte/$effect#$effect.root + * @see {@link https://svelte.dev/docs/svelte/$effect#$effect.root Documentation} */ export function root(fn: () => void | (() => void)): () => void; @@ -3507,6 +3528,9 @@ declare namespace $effect { export const prototype: never; /** @deprecated */ export const toString: never; + + // needed to keep private stuff private + export {}; } /** @@ -3516,7 +3540,7 @@ declare namespace $effect { * let { optionalProp = 42, requiredProp, bindableProp = $bindable() }: { optionalProp?: number; requiredProps: string; bindableProp: boolean } = $props(); * ``` * - * https://svelte.dev/docs/svelte/$props + * @see {@link https://svelte.dev/docs/svelte/$props Documentation} */ declare function $props(): any; @@ -3550,6 +3574,9 @@ declare namespace $props { export const prototype: never; /** @deprecated */ export const toString: never; + + // needed to keep private stuff private + export {}; } /** @@ -3559,7 +3586,7 @@ declare namespace $props { * let { propName = $bindable() }: { propName: boolean } = $props(); * ``` * - * https://svelte.dev/docs/svelte/$bindable + * @see {@link https://svelte.dev/docs/svelte/$bindable Documentation} */ declare function $bindable(fallback?: T): T; @@ -3584,6 +3611,9 @@ declare namespace $bindable { export const prototype: never; /** @deprecated */ export const toString: never; + + // needed to keep private stuff private + export {}; } /** @@ -3602,7 +3632,7 @@ declare namespace $bindable { * $inspect(x, y).with(() => { debugger; }); * ``` * - * https://svelte.dev/docs/svelte/$inspect + * @see {@link https://svelte.dev/docs/svelte/$inspect Documentation} */ declare function $inspect( ...values: T @@ -3646,6 +3676,9 @@ declare namespace $inspect { export const prototype: never; /** @deprecated */ export const toString: never; + + // needed to keep private stuff private + export {}; } /** @@ -3665,7 +3698,7 @@ declare namespace $inspect { * * Only available inside custom element components, and only on the client-side. * - * https://svelte.dev/docs/svelte/$host + * @see {@link https://svelte.dev/docs/svelte/$host Documentation} */ declare function $host(): El; @@ -3690,6 +3723,9 @@ declare namespace $host { export const prototype: never; /** @deprecated */ export const toString: never; + + // needed to keep private stuff private + export {}; } //# sourceMappingURL=index.d.ts.map diff --git a/playgrounds/sandbox/run.js b/playgrounds/sandbox/run.js index 35bffb67a2..f79243d4e7 100644 --- a/playgrounds/sandbox/run.js +++ b/playgrounds/sandbox/run.js @@ -95,6 +95,7 @@ for (const generate of /** @type {const} */ (['client', 'server'])) { if (generate === 'server' || FROM_HTML) { from_html = compile(source, { dev: DEV, + hmr: DEV, filename: input, generate, runes: argv.values.runes, diff --git a/playgrounds/sandbox/scripts/create-test.js b/playgrounds/sandbox/scripts/create-test.js index c733f19419..d183098b22 100644 --- a/playgrounds/sandbox/scripts/create-test.js +++ b/playgrounds/sandbox/scripts/create-test.js @@ -1,6 +1,16 @@ -// Creates a test from the existing playground. cwd needs to be playground/sandbox +// Creates a test from the existing playground. Can be called from anywhere. import fs from 'fs'; import path from 'path'; +import { fileURLToPath } from 'url'; + +// Get the directory where this script is located +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +// Base paths relative to this script's location +const sandbox_dir = path.resolve(__dirname, '..'); +const src_dir = path.join(sandbox_dir, 'src'); +const tests_dir = path.resolve(__dirname, '../../../packages/svelte/tests'); // Get target folder from command line arguments let target_folder = process.argv[2]; @@ -20,7 +30,7 @@ if (!target_folder.startsWith('runtime-')) { process.exit(1); } target_folder = path.join( - path.resolve('../../packages/svelte/tests', target_folder.split('/')[0]), + path.resolve(tests_dir, target_folder.split('/')[0]), 'samples', target_folder.split('/')[1] ); @@ -56,8 +66,8 @@ if (exists) { const files = fs.readdirSync(target_folder); for (const file of files) { if (file !== '_config.js') { - const filePath = path.join(target_folder, file); - fs.rmSync(filePath, { recursive: true, force: true }); + const file_path = path.join(target_folder, file); + fs.rmSync(file_path, { recursive: true, force: true }); } } } else { @@ -65,7 +75,7 @@ if (exists) { } // Starting file -const app_svelte_path = path.resolve('./src/App.svelte'); +const app_svelte_path = path.join(src_dir, 'App.svelte'); const collected_files = new Set(); const processed_files = new Set(); @@ -117,7 +127,7 @@ collect_imports(app_svelte_path); // Copy collected files to target folder for (const file_path of collected_files) { - const relative_path = path.relative(path.resolve('./src'), file_path); + const relative_path = path.relative(src_dir, file_path); let target_path = path.join(target_folder, relative_path); // Rename App.svelte to main.svelte diff --git a/playgrounds/sandbox/scripts/download.js b/playgrounds/sandbox/scripts/download.js index 5678f69ee3..538b4956fe 100644 --- a/playgrounds/sandbox/scripts/download.js +++ b/playgrounds/sandbox/scripts/download.js @@ -1,52 +1,739 @@ import fs from 'node:fs'; +import path from 'node:path'; +import { parseArgs } from 'node:util'; +import { execSync } from 'node:child_process'; +import { chromium } from 'playwright'; -const arg = process.argv[2]; +const { values, positionals } = parseArgs({ + options: { + 'create-test': { + type: 'string' + } + }, + allowPositionals: true +}); -/** @type {URL} */ -let url; +const create_test_name = values['create-test'] ?? null; +const url_arg = positionals[0]; -try { - url = new URL(arg); -} catch (e) { - console.error(`${arg} is not a URL`); +if (!url_arg) { + console.error(`Missing URL argument`); process.exit(1); } -if (url.origin !== 'https://svelte.dev' || !url.pathname.startsWith('/playground/')) { - console.error(`${arg} is not a Svelte playground URL`); - process.exit(1); +const base_dir = import.meta.dirname; + +/** + * Check if the argument is a local directory path + * @param {string} arg + * @returns {boolean} + */ +function is_local_directory(arg) { + try { + return fs.existsSync(arg) && fs.statSync(arg).isDirectory(); + } catch { + return false; + } } -let files; +// Check if it's a local directory first (before URL parsing) +const is_local = is_local_directory(url_arg); + +const resolved_test_path = ['runtime-runes', 'runtime-legacy'] + .map((d) => path.resolve(`${base_dir}/../../../packages/svelte/tests/${d}/samples/${url_arg}`)) + .find(fs.existsSync); -if (url.hash.length > 1) { - const decoded = atob(url.hash.slice(1).replaceAll('-', '+').replaceAll('_', '/')); - // putting it directly into the blob gives a corrupted file - const u8 = new Uint8Array(decoded.length); - for (let i = 0; i < decoded.length; i++) { - u8[i] = decoded.charCodeAt(i); +/** @type {URL | null} */ +let url = null; + +if (!is_local && !resolved_test_path) { + try { + url = new URL(url_arg); + } catch (e) { + console.error(`${url_arg} is not a valid URL or local directory`); + process.exit(1); } - const stream = new Blob([u8]).stream().pipeThrough(new DecompressionStream('gzip')); - const json = await new Response(stream).text(); +} - files = JSON.parse(json).files; -} else { - const id = url.pathname.split('/')[2]; - const response = await fetch(`https://svelte.dev/playground/api/${id}.json`); - - files = (await response.json()).components.map((data) => { - const basename = `${data.name}.${data.type}`; - - return { - type: 'file', - name: basename, - basename, - contents: data.source, - text: true - }; +/** + * Check if URL is a GitHub repository URL + * @param {URL} url + * @returns {boolean} + */ +function is_github_url(url) { + return url.hostname === 'github.com' && url.pathname.split('/').filter(Boolean).length >= 2; +} + +/** + * Check if URL is a StackBlitz GitHub project URL + * @param {URL} url + * @returns {boolean} + */ +function is_stackblitz_github_url(url) { + return url.hostname === 'stackblitz.com' && url.pathname.startsWith('/github/'); +} + +/** + * Check if URL is a StackBlitz edit project URL (non-GitHub) + * @param {URL} url + * @returns {boolean} + */ +function is_stackblitz_edit_url(url) { + return url.hostname === 'stackblitz.com' && url.pathname.startsWith('/edit/'); +} + +/** + * Extract GitHub repo info from a StackBlitz GitHub URL + * @param {URL} url + * @returns {{ owner: string, repo: string, path?: string }} + */ +function extract_stackblitz_github_info(url) { + // URL format: /github/owner/repo or /github/owner/repo/tree/branch/path + const parts = url.pathname.split('/').filter(Boolean); + // parts[0] = 'github', parts[1] = owner, parts[2] = repo + return { + owner: parts[1], + repo: parts[2], + path: parts.length > 3 ? parts.slice(3).join('/') : undefined + }; +} + +/** + * Clone a GitHub repository to a temporary directory + * @param {URL} url + * @param {string} target_dir + */ +function clone_github_repo(url, target_dir) { + // Extract repo URL (handle both https://github.com/owner/repo and https://github.com/owner/repo/tree/branch/path) + const parts = url.pathname.split('/').filter(Boolean); + const owner = parts[0]; + const repo = parts[1]; + const repo_url = `https://github.com/${owner}/${repo}.git`; + + console.log(`Cloning ${repo_url}...`); + execSync(`git clone --depth 1 ${repo_url} "${target_dir}"`, { stdio: 'inherit' }); +} + +/** + * Clone a StackBlitz GitHub project to a temporary directory + * (Converts to regular GitHub clone) + * @param {URL} url + * @param {string} target_dir + */ +function clone_stackblitz_github_project(url, target_dir) { + const info = extract_stackblitz_github_info(url); + const repo_url = `https://github.com/${info.owner}/${info.repo}.git`; + + console.log(`StackBlitz GitHub project detected, cloning from ${repo_url}...`); + + execSync(`git clone --depth 1 ${repo_url} "${target_dir}"`, { stdio: 'inherit' }); +} + +/** + * Download a StackBlitz project using browser automation + * @param {URL} url + * @param {string} target_dir + */ +async function download_stackblitz_project(url, target_dir) { + console.log(`Downloading StackBlitz project via browser automation...`); + console.log(`URL: ${url.href}`); + + const browser = await chromium.launch({ headless: true }); + const context = await browser.newContext({ acceptDownloads: true }); + const page = await context.newPage(); + + try { + // Navigate to the StackBlitz project + console.log('Loading StackBlitz project (this may take a moment)...'); + await page.goto(url.href, { waitUntil: 'domcontentloaded', timeout: 20000 }); + + // Set up download handler + const downloadPromise = page.waitForEvent('download', { timeout: 30000 }); + await page.locator('button[aria-label*="Download Project" i]', { timeout: 30000 }).click(); + console.log('Triggering download...'); + + // Wait for the download to start + const download = await downloadPromise; + + // Save the downloaded file + const zip_path = path.join(target_dir, 'project.zip'); + await download.saveAs(zip_path); + + console.log('Download complete, extracting...'); + + // Extract the zip file + if (process.platform === 'win32') { + execSync( + `powershell -Command "Expand-Archive -Path '${zip_path}' -DestinationPath '${target_dir}' -Force"`, + { stdio: 'inherit' } + ); + } else { + execSync(`unzip -o "${zip_path}" -d "${target_dir}"`, { stdio: 'inherit' }); + } + + // Remove the zip file + fs.unlinkSync(zip_path); + + // Check if files were extracted into a subdirectory + const entries = fs.readdirSync(target_dir); + if (entries.length === 1) { + const subdir = path.join(target_dir, entries[0]); + if (fs.statSync(subdir).isDirectory()) { + // Move files from subdirectory to target_dir + const subentries = fs.readdirSync(subdir); + for (const entry of subentries) { + fs.renameSync(path.join(subdir, entry), path.join(target_dir, entry)); + } + fs.rmdirSync(subdir); + } + } + + console.log('StackBlitz project downloaded successfully'); + } finally { + await browser.close(); + } +} + +/** + * Recursively get all files in a directory + * @param {string} dir + * @param {string} base + * @returns {Array<{path: string, name: string, contents: string}>} + */ +function get_all_files(dir, base = '') { + /** @type {Array<{path: string, name: string, contents: string}>} */ + const results = []; + + if (!fs.existsSync(dir)) return results; + + const entries = fs.readdirSync(dir, { withFileTypes: true }); + + for (const entry of entries) { + const full_path = path.join(dir, entry.name); + const relative_path = base ? `${base}/${entry.name}` : entry.name; + + if (entry.isDirectory()) { + // Skip node_modules, .git, etc. + if (['node_modules', '.git', '.svelte-kit', 'build', 'dist'].includes(entry.name)) { + continue; + } + results.push(...get_all_files(full_path, relative_path)); + } else if ( + entry.name.endsWith('.svelte') || + entry.name.endsWith('.js') || + entry.name.endsWith('.ts') + ) { + results.push({ + path: relative_path, + name: entry.name, + contents: fs.readFileSync(full_path, 'utf-8') + }); + } + } + + return results; +} + +/** + * Detect project type from files + * @param {Array<{path: string, name: string, contents: string}>} files + * @returns {{ type: 'sveltekit' | 'vite', has_app_imports: boolean, has_page_svelte: boolean }} + */ +function detect_project_type(files) { + let has_app_imports = false; + let has_page_svelte = false; + + for (const file of files) { + // Check for $app/* imports + if (/from\s+['"](\$app\/[^'"]+)['"]/.test(file.contents)) { + has_app_imports = true; + } + // Check for +page.svelte or +layout.svelte + if (file.name === '+page.svelte' || file.name === '+layout.svelte') { + has_page_svelte = true; + } + } + + return { + type: has_page_svelte ? 'sveltekit' : 'vite', + has_app_imports, + has_page_svelte + }; +} + +/** + * Convert a route path to a PascalCase component name + * @param {string} route_path - e.g., "about", "blog/post" + * @returns {string} - e.g., "About", "BlogPost" + */ +function route_to_component_name(route_path) { + if (!route_path || route_path === '') return 'Page'; + + return route_path + .split('/') + .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) + .join(''); +} + +/** + * Transform $lib/* imports to relative imports (flattened) + * @param {string} content + * @returns {string} + */ +function transform_lib_imports(content) { + // Replace $lib/ imports with relative paths to flattened files + return content.replace(/from\s+['"](\$lib\/[^'"]+)['"]/g, (match, import_path) => { + // Get just the filename from the lib path + const lib_path = import_path.replace('$lib/', ''); + const filename = path.basename(lib_path); + return `from './${filename}'`; }); } -for (const file of files) { - fs.writeFileSync(`src/${file.name}`, file.contents); +/** + * Build the route tree from SvelteKit files + * @param {Array<{path: string, name: string, contents: string}>} files + * @param {string} routes_prefix - e.g., "src/routes" + * @returns {Map} + */ +function build_route_tree(files, routes_prefix) { + /** @type {Map} */ + const routes = new Map(); + + for (const file of files) { + if (!file.path.startsWith(routes_prefix)) continue; + + const relative_to_routes = file.path.slice(routes_prefix.length + 1); // +1 for the slash + const dir = path.dirname(relative_to_routes); + const route_key = dir === '.' ? '' : dir; + + if (!routes.has(route_key)) { + routes.set(route_key, {}); + } + + const route = routes.get(route_key); + if (file.name === '+layout.svelte') { + route.layout = { path: file.path, contents: file.contents }; + } else if (file.name === '+page.svelte') { + route.page = { path: file.path, contents: file.contents }; + } + } + + return routes; +} + +/** + * Get child routes of a given route + * @param {Map} routes + * @param {string} parent_route + * @returns {string[]} + */ +function get_child_routes(routes, parent_route) { + const children = []; + for (const route of routes.keys()) { + if (route === parent_route) continue; + + const parent_prefix = parent_route === '' ? '' : parent_route + '/'; + if ( + parent_route === '' + ? !route.includes('/') + : route.startsWith(parent_prefix) && !route.slice(parent_prefix.length).includes('/') + ) { + children.push(route); + } + } + return children; +} + +/** + * Transform a layout file's content to replace {@render children()} with a component + * @param {string} content + * @param {string} child_component_name + * @returns {string} + */ +function transform_layout_content(content, child_component_name) { + // Add import for the child component at the top of the script + const import_statement = `import ${child_component_name} from './${child_component_name}.svelte';`; + + // Check if there's already a script tag + if (/]*>/.test(content)) { + // Add import after the opening script tag + content = content.replace(/(]*>)/, `$1\n\t${import_statement}`); + } else { + // Add a new script block at the beginning + content = `\n\n${content}`; + } + + // Replace {@render children()} or {@render children?.()} with the component + content = content.replace(/\{@render\s+children\?\.\(\)\}/g, `<${child_component_name} />`); + content = content.replace(/\{@render\s+children\(\)\}/g, `<${child_component_name} />`); + + return content; +} + +/** + * Convert a SvelteKit project to plain Svelte components + * @param {string} repo_dir + * @returns {Array<{name: string, contents: string}>} + */ +function convert_sveltekit_project(repo_dir) { + const all_files = get_all_files(repo_dir); + /** @type {Array<{name: string, contents: string}>} */ + const output_files = []; + + // Find the routes directory + let routes_prefix = ''; + for (const file of all_files) { + if (file.path.includes('src/routes/')) { + routes_prefix = 'src/routes'; + break; + } + } + + if (!routes_prefix) { + console.error('Could not find src/routes directory'); + process.exit(1); + } + + // Build route tree + const routes = build_route_tree(all_files, routes_prefix); + + // Process lib files - flatten them to root level + const lib_files = all_files.filter((f) => f.path.startsWith('src/lib/')); + for (const file of lib_files) { + // Flatten to just the filename + const new_path = path.basename(file.path); + let contents = file.contents; + contents = transform_lib_imports(contents); + output_files.push({ + name: new_path, + contents + }); + } + + // Sort routes by depth (deepest first) so we can process children before parents + const sorted_routes = [...routes.keys()].sort((a, b) => { + const depth_a = a === '' ? 0 : a.split('/').length; + const depth_b = b === '' ? 0 : b.split('/').length; + return depth_b - depth_a; + }); + + // Map to store what component each route renders + /** @type {Map} */ + const route_component_map = new Map(); + + // First pass: convert all pages + for (const route_key of sorted_routes) { + const route = routes.get(route_key); + if (route?.page) { + const component_name = route_to_component_name(route_key); + let contents = route.page.contents; + contents = transform_lib_imports(contents); + output_files.push({ + name: `${component_name}.svelte`, + contents + }); + // If no layout, this is what the route renders + if (!route.layout) { + route_component_map.set(route_key, component_name); + } + } + } + + // Second pass: convert layouts (from deepest to root) + for (const route_key of sorted_routes) { + const route = routes.get(route_key); + if (route?.layout) { + const is_root = route_key === ''; + const component_name = is_root ? 'App' : route_to_component_name(route_key) + 'Layout'; + + // Determine what child component this layout should render + let child_component = ''; + + // Check if there's a page at this route level + if (route.page) { + child_component = route_to_component_name(route_key); + } else { + // Find child routes that have content + const children = get_child_routes(routes, route_key); + if (children.length > 0) { + // Use the first child's component (or its layout if it has one) + const first_child = children[0]; + child_component = + route_component_map.get(first_child) || route_to_component_name(first_child); + } + } + + let contents = route.layout.contents; + contents = transform_lib_imports(contents); + + if (child_component) { + contents = transform_layout_content(contents, child_component); + } else { + // No child, just remove {@render children()} or {@render children?.()} + contents = contents.replace(/\{@render\s+children\?\.\(\)\}/g, ''); + contents = contents.replace(/\{@render\s+children\(\)\}/g, ''); + } + + output_files.push({ + name: `${component_name}.svelte`, + contents + }); + + // This route now renders the layout + route_component_map.set(route_key, component_name); + } + } + + // If there's no root layout but there's a root page, rename it to App.svelte + if (!routes.get('')?.layout && routes.get('')?.page) { + const page_index = output_files.findIndex((f) => f.name === 'Page.svelte'); + if (page_index !== -1) { + output_files[page_index].name = 'App.svelte'; + } + } + + // If there's no App.svelte yet, create one that imports the first available component + if (!output_files.some((f) => f.name === 'App.svelte')) { + const first_component = output_files.find( + (f) => f.name.endsWith('.svelte') && !f.name.includes('/') + ); + if (first_component) { + const comp_name = first_component.name.replace('.svelte', ''); + output_files.push({ + name: 'App.svelte', + contents: `\n\n<${comp_name} />\n` + }); + } + } + + return output_files; +} + +/** + * Convert a regular Vite+Svelte project + * @param {string} repo_dir + * @returns {Array<{name: string, contents: string}>} + */ +function convert_vite_project(repo_dir) { + const all_files = get_all_files(repo_dir); + /** @type {Array<{name: string, contents: string}>} */ + const output_files = []; + + // Find src directory + const src_files = all_files.filter( + (f) => + f.path.startsWith('src/') && + (f.name.endsWith('.svelte') || f.name.endsWith('.js') || f.name.endsWith('.ts')) + ); + + for (const file of src_files) { + // Flatten all files to root level (just the filename) + const new_path = path.basename(file.path); + + let contents = file.contents; + contents = transform_lib_imports(contents); + + output_files.push({ + name: new_path, + contents + }); + } + + return output_files; +} + +/** + * Process a local or cloned directory + * @param {string} dir_path + * @returns {Array<{name: string, contents: string}>} + */ +function process_directory(dir_path) { + const all_files = get_all_files(dir_path); + const project_info = detect_project_type(all_files); + + console.log(`Detected project type: ${project_info.type}`); + + // Check for $app/* imports + if (project_info.has_app_imports) { + console.error('Error: This SvelteKit project uses $app/* imports which cannot be converted.'); + console.error('The playground does not support SvelteKit runtime features.'); + process.exit(1); + } + + // Convert based on project type + if (project_info.type === 'sveltekit') { + console.log('Converting SvelteKit project to plain Svelte...'); + return convert_sveltekit_project(dir_path); + } else { + console.log('Processing Vite+Svelte project...'); + return convert_vite_project(dir_path); + } +} + +/** + * Reset a directory so it exists and is empty + * @param {string} dir_path + */ +/** + * Create a temporary directory, run an action, and always clean up + * @param {string} base_dir + * @param {(dir: string) => void | Promise} action + */ +async function with_tmp_dir(base_dir, action) { + const tmp_dir = path.join(base_dir, '.tmp-repo'); + + try { + if (fs.existsSync(tmp_dir)) { + fs.rmSync(tmp_dir, { recursive: true, force: true }); + } + fs.mkdirSync(tmp_dir, { recursive: true }); + await action(tmp_dir); + } finally { + if (fs.existsSync(tmp_dir)) { + fs.rmSync(tmp_dir, { recursive: true, force: true }); + } + } +} + +// Main logic +let files; + +// Check if it's a local directory first (before URL parsing) +if (is_local) { + console.log(`Processing local directory: ${url_arg}`); + files = process_directory(url_arg); +} else if (resolved_test_path) { + // Copy files from test + console.log(`Processing test ${url_arg}`); + files = get_all_files(resolved_test_path) + .filter((file) => !file.path.includes('_')) + .map((file) => { + return { + name: file.name === 'main.svelte' ? 'App.svelte' : file.name, + contents: file.contents + }; + }); +} else if (url && is_github_url(url)) { + // GitHub repository handling + await with_tmp_dir(base_dir, (tmp_dir) => { + clone_github_repo(url, tmp_dir); + files = process_directory(tmp_dir); + }); +} else if (url && is_stackblitz_github_url(url)) { + // StackBlitz GitHub project handling (redirect to GitHub clone) + await with_tmp_dir(base_dir, (tmp_dir) => { + clone_stackblitz_github_project(url, tmp_dir); + files = process_directory(tmp_dir); + }); +} else if (url && is_stackblitz_edit_url(url)) { + // StackBlitz edit URLs - use browser automation to download + await with_tmp_dir(base_dir, async (tmp_dir) => { + await download_stackblitz_project(url, tmp_dir); + files = process_directory(tmp_dir); + }); +} else if (url && url.origin === 'https://svelte.dev' && url.pathname.startsWith('/playground/')) { + // Svelte playground URL handling (existing logic) + if (url.hash.length > 1) { + // Decode percent-encoded characters (e.g., %5F => _), and replace base64 chars. + let decoded; + try { + // First, decode URI components to handle %xx encodings (e.g. %5F -> _) (LLMs calling this script sometimes encode them for some reason) + decoded = url.hash.slice(1); + decoded = decodeURIComponent(decoded); + + // Now, restore for base64 (replace -/+, _/ /) + decoded = atob(decoded.replaceAll('-', '+').replaceAll('_', '/')); + } catch (e) { + console.error('Failed to decode URL hash:', e); + process.exit(1); + } + // putting it directly into the blob gives a corrupted file + const u8 = new Uint8Array(decoded.length); + for (let i = 0; i < decoded.length; i++) { + u8[i] = decoded.charCodeAt(i); + } + const stream = new Blob([u8]).stream().pipeThrough(new DecompressionStream('gzip')); + const json = await new Response(stream).text(); + + files = JSON.parse(json).files; + } else { + const id = url.pathname.split('/')[2]; + const response = await fetch(`https://svelte.dev/playground/api/${id}.json`); + + files = (await response.json()).components.map((data) => { + const basename = `${data.name}.${data.type}`; + + return { + type: 'file', + name: basename, + basename, + contents: data.source, + text: true + }; + }); + } +} else { + console.error( + `${url_arg} is not a supported URL (Svelte playground, GitHub repository, or StackBlitz project)` + ); + process.exit(1); +} + +// Output files +if (create_test_name) { + const test_parts = create_test_name.split('/').filter(Boolean); + + if (test_parts.length > 2) { + console.error( + `Invalid test name "${create_test_name}". Expected e.g. "hello-world" or "runtime-legacy/hello-world"` + ); + process.exit(1); + } + + const suite_name = test_parts.length === 2 ? test_parts[0] : 'runtime-runes'; + const test_name = test_parts[test_parts.length - 1]; + + const output_dir = path.join( + base_dir, + '../../..', + 'packages/svelte/tests', + suite_name, + 'samples', + test_name + ); + if (fs.existsSync(output_dir)) { + console.warn(`Test folder "${output_dir}" already exists, overriding its contents`); + fs.rmSync(output_dir, { recursive: true, force: true }); + } + fs.mkdirSync(output_dir, { recursive: true }); + + for (const file of files) { + const output_name = file.name === 'App.svelte' ? 'main.svelte' : file.name; + const output_path = path.join(output_dir, output_name); + + fs.mkdirSync(path.dirname(output_path), { recursive: true }); + fs.writeFileSync(output_path, file.contents); + } + + fs.writeFileSync( + path.join(output_dir, '_config.js'), + `import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + } +}); +` + ); + + console.log(`Test created at ${output_dir}`); +} else { + for (const file of files) { + const output_path = path.join(base_dir, '..', 'src', file.name); + fs.mkdirSync(path.dirname(output_path), { recursive: true }); + fs.writeFileSync(output_path, file.contents); + } + + console.log(`Files written to ${path.join(base_dir, '..', 'src')}`); } diff --git a/playgrounds/sandbox/scripts/hash.js b/playgrounds/sandbox/scripts/hash.js index e70dfc471a..fdfe2d1aa7 100644 --- a/playgrounds/sandbox/scripts/hash.js +++ b/playgrounds/sandbox/scripts/hash.js @@ -1,16 +1,44 @@ import fs from 'node:fs'; +/** + * Detects if a file is text or binary by checking for null bytes + * and validating UTF-8 encoding + * @param {string} filepath - Path to the file + * @returns {boolean} - true if file is text, false if binary + */ +function is_text_file(filepath) { + const buffer = fs.readFileSync(filepath); + // Check for null bytes which indicate binary files + for (let i = 0; i < buffer.length; i++) { + if (buffer[i] === 0) { + return false; + } + } + // Validate UTF-8 encoding + try { + const text = buffer.toString('utf-8'); + // Verify round-trip encoding to ensure valid UTF-8 + const encoded = Buffer.from(text, 'utf-8'); + return buffer.equals(encoded); + } catch { + return false; + } +} + const files = []; for (const basename of fs.readdirSync('src')) { if (fs.statSync(`src/${basename}`).isDirectory()) continue; + const filepath = `src/${basename}`; + const text = is_text_file(filepath); + files.push({ type: 'file', name: basename, basename, - contents: fs.readFileSync(`src/${basename}`, 'utf-8'), - text: true // TODO might not be + contents: fs.readFileSync(filepath, text ? 'utf-8' : 'base64'), + text }); } diff --git a/playgrounds/sandbox/svelte.config.js b/playgrounds/sandbox/svelte.config.js index 68ac605385..33e8c98908 100644 --- a/playgrounds/sandbox/svelte.config.js +++ b/playgrounds/sandbox/svelte.config.js @@ -1,5 +1,8 @@ export default { + /** @type {import('svelte/compiler').CompileOptions} */ compilerOptions: { + css: 'injected', + hmr: false, experimental: { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 05298fe8e3..48c2a52cc5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,9 +11,12 @@ importers: '@changesets/cli': specifier: ^2.29.8 version: 2.29.8(@types/node@20.19.17) + '@eslint/js': + specifier: ^10.0.0 + version: 10.0.1(eslint@10.0.0) '@sveltejs/eslint-config': - specifier: ^8.3.3 - version: 8.3.3(@stylistic/eslint-plugin-js@1.8.0(eslint@9.9.1))(eslint-config-prettier@9.1.0(eslint@9.9.1))(eslint-plugin-n@17.16.1(eslint@9.9.1)(typescript@5.5.4))(eslint-plugin-svelte@3.11.0(eslint@9.9.1)(svelte@packages+svelte))(eslint@9.9.1)(typescript-eslint@8.48.1(eslint@9.9.1)(typescript@5.5.4))(typescript@5.5.4) + specifier: ^8.3.5 + version: 8.3.5(@stylistic/eslint-plugin-js@1.8.0(eslint@10.0.0))(eslint-config-prettier@9.1.0(eslint@10.0.0))(eslint-plugin-n@17.16.1(eslint@10.0.0)(typescript@5.5.4))(eslint-plugin-svelte@3.15.0(eslint@10.0.0)(svelte@packages+svelte))(eslint@10.0.0)(typescript-eslint@8.55.0(eslint@10.0.0)(typescript@5.5.4))(typescript@5.5.4) '@svitejs/changesets-changelog-github-compact': specifier: ^1.1.0 version: 1.1.0 @@ -27,20 +30,20 @@ importers: specifier: ^2.1.9 version: 2.1.9(vitest@2.1.9(@types/node@20.19.17)(jsdom@25.0.1)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)) eslint: - specifier: ^9.9.1 - version: 9.9.1 + specifier: ^10.0.0 + version: 10.0.0 eslint-plugin-lube: - specifier: ^0.4.3 - version: 0.4.3 + specifier: ^0.5.1 + version: 0.5.1(eslint@10.0.0) eslint-plugin-svelte: - specifier: ^3.11.0 - version: 3.11.0(eslint@9.9.1)(svelte@packages+svelte) + specifier: ^3.15.0 + version: 3.15.0(eslint@10.0.0)(svelte@packages+svelte) jsdom: specifier: 25.0.1 version: 25.0.1 playwright: - specifier: ^1.46.1 - version: 1.46.1 + specifier: ^1.58.0 + version: 1.58.0 prettier: specifier: ^3.2.4 version: 3.2.4 @@ -54,8 +57,8 @@ importers: specifier: ^5.5.4 version: 5.5.4 typescript-eslint: - specifier: ^8.48.1 - version: 8.48.1(eslint@9.9.1)(typescript@5.5.4) + specifier: ^8.55.0 + version: 8.55.0(eslint@10.0.0)(typescript@5.5.4) v8-natives: specifier: ^1.2.5 version: 1.2.5 @@ -90,14 +93,14 @@ importers: specifier: ^2.1.1 version: 2.1.1 devalue: - specifier: ^5.5.0 - version: 5.5.0 + specifier: ^5.6.2 + version: 5.6.2 esm-env: specifier: ^1.2.1 version: 1.2.1 esrap: - specifier: ^2.2.1 - version: 2.2.1 + specifier: ^2.2.2 + version: 2.2.2 is-reference: specifier: ^3.0.3 version: 3.0.3 @@ -115,8 +118,8 @@ importers: specifier: ^0.3.25 version: 0.3.31 '@playwright/test': - specifier: ^1.46.1 - version: 1.46.1 + specifier: ^1.58.0 + version: 1.58.0 '@rollup/plugin-commonjs': specifier: ^28.0.1 version: 28.0.1(rollup@4.52.5) @@ -570,8 +573,8 @@ packages: cpu: [x64] os: [win32] - '@eslint-community/eslint-utils@4.9.0': - resolution: {integrity: sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==} + '@eslint-community/eslint-utils@4.9.1': + resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 @@ -580,28 +583,49 @@ packages: resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} - '@eslint/config-array@0.18.0': - resolution: {integrity: sha512-fTxvnS1sRMu3+JjXwJG0j/i4RT9u4qJ+lqS/yCGap4lH4zZGzQ7tu+xZqQmcMZq5OBZDL4QRxQzRjkWcGt8IVw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/config-array@0.23.1': + resolution: {integrity: sha512-uVSdg/V4dfQmTjJzR0szNczjOH/J+FyUMMjYtr07xFRXR7EDf9i1qdxrD0VusZH9knj1/ecxzCQQxyic5NzAiA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} - '@eslint/eslintrc@3.1.0': - resolution: {integrity: sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/config-helpers@0.5.2': + resolution: {integrity: sha512-a5MxrdDXEvqnIq+LisyCX6tQMPF/dSJpCfBgBauY+pNZ28yCtSsTvyTYrMhaI+LK26bVyCJfJkT0u8KIj2i1dQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} - '@eslint/js@9.9.1': - resolution: {integrity: sha512-xIDQRsfg5hNBqHz04H1R3scSVwmI+KUbqjsQKHKQ1DAUSaUjYPReZZmS/5PNiKu1fUvzDd6H7DEDKACSEhu+TQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/core@1.1.0': + resolution: {integrity: sha512-/nr9K9wkr3P1EzFTdFdMoLuo1PmIxjmwvPozwoSodjNBdefGujXQUF93u1DDZpEaTuDvMsIQddsd35BwtrW9Xw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} - '@eslint/object-schema@2.1.4': - resolution: {integrity: sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/js@10.0.1': + resolution: {integrity: sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + peerDependencies: + eslint: ^10.0.0 + peerDependenciesMeta: + eslint: + optional: true + + '@eslint/object-schema@3.0.1': + resolution: {integrity: sha512-P9cq2dpr+LU8j3qbLygLcSZrl2/ds/pUpfnHNNuk5HW7mnngHs+6WSq5C9mO3rqRX8A1poxqLTC9cu0KOyJlBg==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/plugin-kit@0.6.0': + resolution: {integrity: sha512-bIZEUzOI1jkhviX2cp5vNyXQc6olzb2ohewQubuYlMXZ2Q/XjBO0x0XhGPvc9fjSIiUN0vw+0hq53BJ4eQSJKQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@humanfs/core@0.19.1': + resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.7': + resolution: {integrity: sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==} + engines: {node: '>=18.18.0'} '@humanwhocodes/module-importer@1.0.1': resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} engines: {node: '>=12.22'} - '@humanwhocodes/retry@0.3.0': - resolution: {integrity: sha512-d2CGZR2o7fS6sWB7DG/3a95bGKQyHMACZ5aW8qGkkqQpUoZV6C0X7Pc7l4ZNMZkfNBf4VWNe9E1jRsf0G146Ew==} + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} engines: {node: '>=18.18'} '@inquirer/external-editor@1.0.2': @@ -613,6 +637,14 @@ packages: '@types/node': optional: true + '@isaacs/balanced-match@4.0.1': + resolution: {integrity: sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==} + engines: {node: 20 || >=22} + + '@isaacs/brace-expansion@5.0.1': + resolution: {integrity: sha512-WMz71T1JS624nWj2n2fnYAuPovhv7EUhk69R6i9dsVyzxt5eM3bjwvgk9L+APE1TRscGysAVMANkB0jh0LQZrQ==} + engines: {node: 20 || >=22} + '@isaacs/cliui@8.0.2': resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} @@ -667,8 +699,8 @@ packages: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} - '@playwright/test@1.46.1': - resolution: {integrity: sha512-Fq6SwLujA/DOIvNC2EL/SojJnkKf/rAwJ//APpJJHRyMi1PdKrY3Az+4XNQ51N4RTbItbIByQ0jgd1tayq1aeA==} + '@playwright/test@1.58.0': + resolution: {integrity: sha512-fWza+Lpbj6SkQKCrU6si4iu+fD2dD3gxNHFhUPxsfXBPhnv3rRSQVd0NtBUT9Z/RhF/boCBcuUaMUSTRTopjZg==} engines: {node: '>=18'} hasBin: true @@ -841,8 +873,8 @@ packages: peerDependencies: acorn: ^8.9.0 - '@sveltejs/eslint-config@8.3.3': - resolution: {integrity: sha512-vkrQgEmhokFEOpuTo7NlVXJJMJJGNzxjmkQCTkHSwIOdzQSUukDIJ4038IjdcnIERSIlo4OpLAydWLx52BVyQA==} + '@sveltejs/eslint-config@8.3.5': + resolution: {integrity: sha512-f8g5v1GNQePIJq6Rh3mf0riXx+7+IsRaTS7nn8w7r2RZFPPkuBAHGcHST9BTJOQb5UCSnPmst7BJHEDIlVtP3A==} peerDependencies: '@stylistic/eslint-plugin-js': '>= 1' eslint: '>= 9' @@ -877,6 +909,9 @@ packages: '@types/eslint@8.56.12': resolution: {integrity: sha512-03ruubjWyOHlmljCVoxSuNDdmfZDzsrrz0P2LeJsOXr+ZwFQ+0yQIwNCwt/GYhV7Z31fgtXJTAEs+FYlEL851g==} + '@types/esrecurse@4.3.1': + resolution: {integrity: sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==} + '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} @@ -898,63 +933,63 @@ packages: '@types/resolve@1.20.2': resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==} - '@typescript-eslint/eslint-plugin@8.48.1': - resolution: {integrity: sha512-X63hI1bxl5ohelzr0LY5coufyl0LJNthld+abwxpCoo6Gq+hSqhKwci7MUWkXo67mzgUK6YFByhmaHmUcuBJmA==} + '@typescript-eslint/eslint-plugin@8.55.0': + resolution: {integrity: sha512-1y/MVSz0NglV1ijHC8OT49mPJ4qhPYjiK08YUQVbIOyu+5k862LKUHFkpKHWu//zmr7hDR2rhwUm6gnCGNmGBQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - '@typescript-eslint/parser': ^8.48.1 + '@typescript-eslint/parser': ^8.55.0 eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/parser@8.48.1': - resolution: {integrity: sha512-PC0PDZfJg8sP7cmKe6L3QIL8GZwU5aRvUFedqSIpw3B+QjRSUZeeITC2M5XKeMXEzL6wccN196iy3JLwKNvDVA==} + '@typescript-eslint/parser@8.55.0': + resolution: {integrity: sha512-4z2nCSBfVIMnbuu8uinj+f0o4qOeggYJLbjpPHka3KH1om7e+H9yLKTYgksTaHcGco+NClhhY2vyO3HsMH1RGw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/project-service@8.48.1': - resolution: {integrity: sha512-HQWSicah4s9z2/HifRPQ6b6R7G+SBx64JlFQpgSSHWPKdvCZX57XCbszg/bapbRsOEv42q5tayTYcEFpACcX1w==} + '@typescript-eslint/project-service@8.55.0': + resolution: {integrity: sha512-zRcVVPFUYWa3kNnjaZGXSu3xkKV1zXy8M4nO/pElzQhFweb7PPtluDLQtKArEOGmjXoRjnUZ29NjOiF0eCDkcQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/scope-manager@8.48.1': - resolution: {integrity: sha512-rj4vWQsytQbLxC5Bf4XwZ0/CKd362DkWMUkviT7DCS057SK64D5lH74sSGzhI6PDD2HCEq02xAP9cX68dYyg1w==} + '@typescript-eslint/scope-manager@8.55.0': + resolution: {integrity: sha512-fVu5Omrd3jeqeQLiB9f1YsuK/iHFOwb04bCtY4BSCLgjNbOD33ZdV6KyEqplHr+IlpgT0QTZ/iJ+wT7hvTx49Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/tsconfig-utils@8.48.1': - resolution: {integrity: sha512-k0Jhs4CpEffIBm6wPaCXBAD7jxBtrHjrSgtfCjUvPp9AZ78lXKdTR8fxyZO5y4vWNlOvYXRtngSZNSn+H53Jkw==} + '@typescript-eslint/tsconfig-utils@8.55.0': + resolution: {integrity: sha512-1R9cXqY7RQd7WuqSN47PK9EDpgFUK3VqdmbYrvWJZYDd0cavROGn+74ktWBlmJ13NXUQKlZ/iAEQHI/V0kKe0Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/type-utils@8.48.1': - resolution: {integrity: sha512-1jEop81a3LrJQLTf/1VfPQdhIY4PlGDBc/i67EVWObrtvcziysbLN3oReexHOM6N3jyXgCrkBsZpqwH0hiDOQg==} + '@typescript-eslint/type-utils@8.55.0': + resolution: {integrity: sha512-x1iH2unH4qAt6I37I2CGlsNs+B9WGxurP2uyZLRz6UJoZWDBx9cJL1xVN/FiOmHEONEg6RIufdvyT0TEYIgC5g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/types@8.48.1': - resolution: {integrity: sha512-+fZ3LZNeiELGmimrujsDCT4CRIbq5oXdHe7chLiW8qzqyPMnn1puNstCrMNVAqwcl2FdIxkuJ4tOs/RFDBVc/Q==} + '@typescript-eslint/types@8.55.0': + resolution: {integrity: sha512-ujT0Je8GI5BJWi+/mMoR0wxwVEQaxM+pi30xuMiJETlX80OPovb2p9E8ss87gnSVtYXtJoU9U1Cowcr6w2FE0w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/typescript-estree@8.48.1': - resolution: {integrity: sha512-/9wQ4PqaefTK6POVTjJaYS0bynCgzh6ClJHGSBj06XEHjkfylzB+A3qvyaXnErEZSaxhIo4YdyBgq6j4RysxDg==} + '@typescript-eslint/typescript-estree@8.55.0': + resolution: {integrity: sha512-EwrH67bSWdx/3aRQhCoxDaHM+CrZjotc2UCCpEDVqfCE+7OjKAGWNY2HsCSTEVvWH2clYQK8pdeLp42EVs+xQw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/utils@8.48.1': - resolution: {integrity: sha512-fAnhLrDjiVfey5wwFRwrweyRlCmdz5ZxXz2G/4cLn0YDLjTapmN4gcCsTBR1N2rWnZSDeWpYtgLDsJt+FpmcwA==} + '@typescript-eslint/utils@8.55.0': + resolution: {integrity: sha512-BqZEsnPGdYpgyEIkDC1BadNY8oMwckftxBT+C8W0g1iKPdeqKZBtTfnvcq0nf60u7MkjFO8RBvpRGZBPw4L2ow==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/visitor-keys@8.48.1': - resolution: {integrity: sha512-BmxxndzEWhE4TIEEMBs8lP3MBWN3jFPs/p6gPm/wkv02o41hI6cq9AuSmGAaTTHPtA1FTi2jBre4A9rm5ZmX+Q==} + '@typescript-eslint/visitor-keys@8.55.0': + resolution: {integrity: sha512-AxNRwEie8Nn4eFS1FzDMJWIISMGoXMb037sgCBJ3UR6o0fQTzr2tqN9WT+DkWJPhIdQCfV7T6D387566VtnCJA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@vitest/coverage-v8@2.1.9': @@ -1079,9 +1114,6 @@ packages: birpc@2.5.0: resolution: {integrity: sha512-VSWO/W6nNQdyP520F1mhf+Lc2f8pjGQOtoHHm7Ze8Go1kX7akpVIrtTa0fn+HB0QJEDVacl6aO08YE0PgXfdnQ==} - brace-expansion@1.1.12: - resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} - brace-expansion@2.0.2: resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} @@ -1100,18 +1132,10 @@ packages: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} - callsites@3.1.0: - resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} - engines: {node: '>=6'} - chai@5.3.3: resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} engines: {node: '>=18'} - chalk@4.1.2: - resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} - engines: {node: '>=10'} - chardet@2.1.0: resolution: {integrity: sha512-bNFETTG/pM5ryzQ9Ad0lJOTa6HWD/YsScAR3EnCPZRPlQh77JocYktSHOUHelyhm8IARL+o4c4F1bP5KVOjiRA==} @@ -1148,9 +1172,6 @@ packages: commondir@1.0.1: resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==} - concat-map@0.0.1: - resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} - cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -1219,8 +1240,8 @@ packages: engines: {node: '>=0.10'} hasBin: true - devalue@5.5.0: - resolution: {integrity: sha512-69sM5yrHfFLJt0AZ9QqZXGCPfJ7fQjvpln3Rq5+PS03LD32Ost1Q9N+eEnaQwGRIriKkMImXD56ocjQmfjbV3w==} + devalue@5.6.2: + resolution: {integrity: sha512-nPRkjWzzDQlsejL1WVifk5rvcFi/y1onBRxjaFMjZeR9mFpqu2gmAZ9xUB9/IEanEP/vBtGeGganC/GO1fmufg==} dir-glob@3.0.1: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} @@ -1295,8 +1316,10 @@ packages: peerDependencies: eslint: '>=8' - eslint-plugin-lube@0.4.3: - resolution: {integrity: sha512-BVO83tRo090d6a04cl45Gb761SD79cOT6wKxxWrpsH7Rv8I0SJvc79ijE11vvyxxCMiGUVq/w4NqqPJAHyYfSQ==} + eslint-plugin-lube@0.5.1: + resolution: {integrity: sha512-z01UM0LAI/Z+b9qGoVqN3RNycXbYXi6i4/QT0He58LdBVmmOd+z4tJQmvOqEh3A4TVtSQcGYq4/4wX/OebGISQ==} + peerDependencies: + eslint: '>=9.0.0' eslint-plugin-n@17.16.1: resolution: {integrity: sha512-/7FVAwjUrix9P5lycnsYRIQRwFo/DZROD+ZXWLpE+/EZWLyuLvyFaRdAPYJSz+nlAdZIZp+LAzlBerQSVYUNFg==} @@ -1304,11 +1327,11 @@ packages: peerDependencies: eslint: '>=8.23.0' - eslint-plugin-svelte@3.11.0: - resolution: {integrity: sha512-KliWlkieHyEa65aQIkRwUFfHzT5Cn4u3BQQsu3KlkJOs7c1u7ryn84EWaOjEzilbKgttT4OfBURA8Uc4JBSQIw==} + eslint-plugin-svelte@3.15.0: + resolution: {integrity: sha512-QKB7zqfuB8aChOfBTComgDptMf2yxiJx7FE04nneCmtQzgTHvY8UJkuh8J2Rz7KB9FFV9aTHX6r7rdYGvG8T9Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - eslint: ^8.57.1 || ^9.0.0 + eslint: ^8.57.1 || ^9.0.0 || ^10.0.0 svelte: ^3.37.0 || ^4.0.0 || ^5.0.0 peerDependenciesMeta: svelte: @@ -1318,6 +1341,10 @@ packages: resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + eslint-scope@9.1.0: + resolution: {integrity: sha512-CkWE42hOJsNj9FJRaoMX9waUFYhqY4jmyLFdAdzZr6VaCg3ynLYx4WnOdkaIifGfH4gsUcBTn4OZbHXkpLD0FQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + eslint-visitor-keys@3.4.3: resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -1326,9 +1353,13 @@ packages: resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - eslint@9.9.1: - resolution: {integrity: sha512-dHvhrbfr4xFQ9/dq+jcVneZMyRYLjggWjk6RVsIiHsP8Rz6yZ8LvZ//iU4TrZF+SXWG+JkNF2OyiZRvzgRDqMg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + eslint-visitor-keys@5.0.0: + resolution: {integrity: sha512-A0XeIi7CXU7nPlfHS9loMYEKxUaONu/hTEzHTGba9Huu94Cq1hPivf+DE5erJozZOky0LfvXAyrV/tcswpLI0Q==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + eslint@10.0.0: + resolution: {integrity: sha512-O0piBKY36YSJhlFSG8p9VUdPV/SxxS4FYDWVpr/9GJuMaepzwlf4J8I4ov1b+ySQfDTPhc3DtLaxcT1fN0yqCg==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} hasBin: true peerDependencies: jiti: '*' @@ -1343,6 +1374,10 @@ packages: resolution: {integrity: sha512-M1M6CpiE6ffoigIOWYO9UDP8TMUw9kqb21tf+08IgDYjCsOvCuDt4jQcZmoYxx+w7zlKw9/N0KXfto+I8/FrXA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + espree@11.1.0: + resolution: {integrity: sha512-WFWYhO1fV4iYkqOOvq8FbqIhr2pYfoDY0kCotMkDeNtGpiGGkZ1iov2u8ydjtgM8yF8rzK7oaTbw2NAzbAbehw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + espree@9.6.1: resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -1352,12 +1387,12 @@ packages: engines: {node: '>=4'} hasBin: true - esquery@1.5.0: - resolution: {integrity: sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==} + esquery@1.7.0: + resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} engines: {node: '>=0.10'} - esrap@2.2.1: - resolution: {integrity: sha512-GiYWG34AN/4CUyaWAgunGt0Rxvr1PTMlGC0vvEov/uOQYWne2bpN03Um+k8jT+q3op33mKouP2zeJ6OlM+qeUg==} + esrap@2.2.2: + resolution: {integrity: sha512-zA6497ha+qKvoWIK+WM9NAh5ni17sKZKhbS5B3PoYbBvaYHZWoS33zmFybmyqpn07RLUxSmn+RCls2/XF+d0oQ==} esrecurse@4.3.0: resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} @@ -1474,12 +1509,9 @@ packages: glob@10.5.0: resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me hasBin: true - globals@14.0.0: - resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} - engines: {node: '>=18'} - globals@15.15.0: resolution: {integrity: sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==} engines: {node: '>=18'} @@ -1488,6 +1520,10 @@ packages: resolution: {integrity: sha512-bqWEnJ1Nt3neqx2q5SFfGS8r/ahumIakg3HcwtNlrVlwXIeNumWn/c7Pn/wKzGhf6SaW6H6uWXLqC30STCMchQ==} engines: {node: '>=18'} + globals@17.3.0: + resolution: {integrity: sha512-yMqGUQVVCkD4tqjOJf3TnrvaaHDMYp4VlUSObbkIiuCPe/ofdMBFIAcBbCSRFWOnos6qRiTVStDwqPLUclaxIw==} + engines: {node: '>=18'} + globby@11.1.0: resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} engines: {node: '>=10'} @@ -1495,9 +1531,6 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} - graphemer@1.4.0: - resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} - has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} @@ -1544,10 +1577,6 @@ packages: immutable@4.3.7: resolution: {integrity: sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw==} - import-fresh@3.3.0: - resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} - engines: {node: '>=6'} - imurmurhash@0.1.4: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} @@ -1588,10 +1617,6 @@ packages: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} - is-path-inside@3.0.3: - resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} - engines: {node: '>=8'} - is-potential-custom-element-name@1.0.1: resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} @@ -1751,9 +1776,6 @@ packages: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} - lodash.merge@4.6.2: - resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} - lodash.startcase@4.4.0: resolution: {integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==} @@ -1789,8 +1811,9 @@ packages: resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} engines: {node: '>= 0.6'} - minimatch@3.1.2: - resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + minimatch@10.1.2: + resolution: {integrity: sha512-fu656aJ0n2kcXwsnwnv9g24tkU5uSmOlTjd6WyyaKm2Z+h1qmY6bAjrcaIxF/BslFqbZ8UBtbJi7KgQOZD2PTw==} + engines: {node: 20 || >=22} minimatch@9.0.5: resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} @@ -1883,10 +1906,6 @@ packages: package-manager-detector@0.2.11: resolution: {integrity: sha512-BEnLolu+yuz22S56CU1SUKq3XC3PkwD5wv4ikR4MfGvnRVcmzXR9DwSlW2fEamyTPyXHomBJRzgapeuBvRNzJQ==} - parent-module@1.0.1: - resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} - engines: {node: '>=6'} - parse5@7.1.2: resolution: {integrity: sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==} @@ -1937,13 +1956,13 @@ packages: resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==} engines: {node: '>=6'} - playwright-core@1.46.1: - resolution: {integrity: sha512-h9LqIQaAv+CYvWzsZ+h3RsrqCStkBHlgo6/TJlFst3cOTlLghBQlJwPOZKQJTKNaD3QIB7aAVQ+gfWbN3NXB7A==} + playwright-core@1.58.0: + resolution: {integrity: sha512-aaoB1RWrdNi3//rOeKuMiS65UCcgOVljU46At6eFcOFPFHWtd2weHRRow6z/n+Lec0Lvu0k9ZPKJSjPugikirw==} engines: {node: '>=18'} hasBin: true - playwright@1.46.1: - resolution: {integrity: sha512-oPcr1yqoXLCkgKtD5eNUPLiN40rYEM39odNpIb6VE6S7/15gJmA1NzVv6zJYusV0e7tzvkU/utBFNa/Kpxmwng==} + playwright@1.58.0: + resolution: {integrity: sha512-2SVA0sbPktiIY/MCOPX8e86ehA/e+tDNq+e5Y8qjKYti2Z/JG7xnronT/TXTIkKbYGWlCbuucZ6dziEgkoEjQQ==} engines: {node: '>=18'} hasBin: true @@ -2028,10 +2047,6 @@ packages: resolution: {integrity: sha512-RSYAtP31mvYLkAHrOlh25pCNQ5hWnT106VukGaaFfuJrZFkGRX5GhUAdPqpSDXxOhA2c4akmRuplv1mRqnBn6Q==} engines: {node: '>=8'} - resolve-from@4.0.0: - resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} - engines: {node: '>=4'} - resolve-from@5.0.0: resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} engines: {node: '>=8'} @@ -2162,10 +2177,6 @@ packages: resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} engines: {node: '>=4'} - strip-json-comments@3.1.1: - resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} - engines: {node: '>=8'} - supports-color@7.2.0: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} @@ -2174,9 +2185,9 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} - svelte-eslint-parser@1.3.0: - resolution: {integrity: sha512-VCgMHKV7UtOGcGLGNFSbmdm6kEKjtzo5nnpGU/mnx4OsFY6bZ7QwRF5DUx+Hokw5Lvdyo8dpk8B1m8mliomrNg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + svelte-eslint-parser@1.4.1: + resolution: {integrity: sha512-1eqkfQ93goAhjAXxZiu1SaKI9+0/sxp4JIWQwUpsz7ybehRE5L8dNuz7Iry7K22R47p5/+s9EM+38nHV2OlgXA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0, pnpm: 10.24.0} peerDependencies: svelte: ^3.37.0 || ^4.0.0 || ^5.0.0 peerDependenciesMeta: @@ -2203,9 +2214,6 @@ packages: resolution: {integrity: sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==} engines: {node: '>=18'} - text-table@0.2.0: - resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} - tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} @@ -2268,8 +2276,8 @@ packages: peerDependencies: typescript: '>=4.2.0' - ts-api-utils@2.1.0: - resolution: {integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==} + ts-api-utils@2.4.0: + resolution: {integrity: sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==} engines: {node: '>=18.12'} peerDependencies: typescript: '>=4.8.4' @@ -2283,8 +2291,8 @@ packages: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} - typescript-eslint@8.48.1: - resolution: {integrity: sha512-FbOKN1fqNoXp1hIl5KYpObVrp0mCn+CLgn479nmu2IsRMrx2vyv74MmsBLVlhg8qVwNFGbXSp8fh1zp8pEoC2A==} + typescript-eslint@8.55.0: + resolution: {integrity: sha512-HE4wj+r5lmDVS9gdaN0/+iqNvPZwGfnJ5lZuz7s5vLlg9ODw0bIiiETaios9LvFI1U94/VBXGm3CB2Y5cNFMpw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 @@ -2471,6 +2479,7 @@ packages: whatwg-encoding@3.1.1: resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} engines: {node: '>=18'} + deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation whatwg-mimetype@4.0.0: resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} @@ -2860,42 +2869,50 @@ snapshots: '@esbuild/win32-x64@0.25.11': optional: true - '@eslint-community/eslint-utils@4.9.0(eslint@9.9.1)': + '@eslint-community/eslint-utils@4.9.1(eslint@10.0.0)': dependencies: - eslint: 9.9.1 + eslint: 10.0.0 eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.2': {} - '@eslint/config-array@0.18.0': + '@eslint/config-array@0.23.1': dependencies: - '@eslint/object-schema': 2.1.4 + '@eslint/object-schema': 3.0.1 debug: 4.4.3 - minimatch: 3.1.2 + minimatch: 10.1.2 transitivePeerDependencies: - supports-color - '@eslint/eslintrc@3.1.0': + '@eslint/config-helpers@0.5.2': dependencies: - ajv: 6.12.6 - debug: 4.4.3 - espree: 10.1.0 - globals: 14.0.0 - ignore: 5.3.2 - import-fresh: 3.3.0 - js-yaml: 4.1.1 - minimatch: 3.1.2 - strip-json-comments: 3.1.1 - transitivePeerDependencies: - - supports-color + '@eslint/core': 1.1.0 - '@eslint/js@9.9.1': {} + '@eslint/core@1.1.0': + dependencies: + '@types/json-schema': 7.0.15 - '@eslint/object-schema@2.1.4': {} + '@eslint/js@10.0.1(eslint@10.0.0)': + optionalDependencies: + eslint: 10.0.0 + + '@eslint/object-schema@3.0.1': {} + + '@eslint/plugin-kit@0.6.0': + dependencies: + '@eslint/core': 1.1.0 + levn: 0.4.1 + + '@humanfs/core@0.19.1': {} + + '@humanfs/node@0.16.7': + dependencies: + '@humanfs/core': 0.19.1 + '@humanwhocodes/retry': 0.4.3 '@humanwhocodes/module-importer@1.0.1': {} - '@humanwhocodes/retry@0.3.0': {} + '@humanwhocodes/retry@0.4.3': {} '@inquirer/external-editor@1.0.2(@types/node@20.19.17)': dependencies: @@ -2904,6 +2921,12 @@ snapshots: optionalDependencies: '@types/node': 20.19.17 + '@isaacs/balanced-match@4.0.1': {} + + '@isaacs/brace-expansion@5.0.1': + dependencies: + '@isaacs/balanced-match': 4.0.1 + '@isaacs/cliui@8.0.2': dependencies: string-width: 5.1.2 @@ -2973,9 +2996,9 @@ snapshots: '@pkgjs/parseargs@0.11.0': optional: true - '@playwright/test@1.46.1': + '@playwright/test@1.58.0': dependencies: - playwright: 1.46.1 + playwright: 1.58.0 '@polka/url@1.0.0-next.25': {} @@ -3087,12 +3110,12 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.52.5': optional: true - '@stylistic/eslint-plugin-js@1.8.0(eslint@9.9.1)': + '@stylistic/eslint-plugin-js@1.8.0(eslint@10.0.0)': dependencies: '@types/eslint': 8.56.12 acorn: 8.15.0 escape-string-regexp: 4.0.0 - eslint: 9.9.1 + eslint: 10.0.0 eslint-visitor-keys: 3.4.3 espree: 9.6.1 @@ -3100,16 +3123,16 @@ snapshots: dependencies: acorn: 8.15.0 - '@sveltejs/eslint-config@8.3.3(@stylistic/eslint-plugin-js@1.8.0(eslint@9.9.1))(eslint-config-prettier@9.1.0(eslint@9.9.1))(eslint-plugin-n@17.16.1(eslint@9.9.1)(typescript@5.5.4))(eslint-plugin-svelte@3.11.0(eslint@9.9.1)(svelte@packages+svelte))(eslint@9.9.1)(typescript-eslint@8.48.1(eslint@9.9.1)(typescript@5.5.4))(typescript@5.5.4)': + '@sveltejs/eslint-config@8.3.5(@stylistic/eslint-plugin-js@1.8.0(eslint@10.0.0))(eslint-config-prettier@9.1.0(eslint@10.0.0))(eslint-plugin-n@17.16.1(eslint@10.0.0)(typescript@5.5.4))(eslint-plugin-svelte@3.15.0(eslint@10.0.0)(svelte@packages+svelte))(eslint@10.0.0)(typescript-eslint@8.55.0(eslint@10.0.0)(typescript@5.5.4))(typescript@5.5.4)': dependencies: - '@stylistic/eslint-plugin-js': 1.8.0(eslint@9.9.1) - eslint: 9.9.1 - eslint-config-prettier: 9.1.0(eslint@9.9.1) - eslint-plugin-n: 17.16.1(eslint@9.9.1)(typescript@5.5.4) - eslint-plugin-svelte: 3.11.0(eslint@9.9.1)(svelte@packages+svelte) - globals: 15.15.0 + '@stylistic/eslint-plugin-js': 1.8.0(eslint@10.0.0) + eslint: 10.0.0 + eslint-config-prettier: 9.1.0(eslint@10.0.0) + eslint-plugin-n: 17.16.1(eslint@10.0.0)(typescript@5.5.4) + eslint-plugin-svelte: 3.15.0(eslint@10.0.0)(svelte@packages+svelte) + globals: 17.3.0 typescript: 5.5.4 - typescript-eslint: 8.48.1(eslint@9.9.1)(typescript@5.5.4) + typescript-eslint: 8.55.0(eslint@10.0.0)(typescript@5.5.4) '@sveltejs/vite-plugin-svelte-inspector@5.0.1(@sveltejs/vite-plugin-svelte@6.2.0(svelte@packages+svelte)(vite@7.1.11(@types/node@24.5.2)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)))(svelte@packages+svelte)(vite@7.1.11(@types/node@24.5.2)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0))': dependencies: @@ -3146,6 +3169,8 @@ snapshots: '@types/estree': 1.0.8 '@types/json-schema': 7.0.15 + '@types/esrecurse@4.3.1': {} + '@types/estree@1.0.8': {} '@types/json-schema@7.0.15': {} @@ -3164,96 +3189,95 @@ snapshots: '@types/resolve@1.20.2': {} - '@typescript-eslint/eslint-plugin@8.48.1(@typescript-eslint/parser@8.48.1(eslint@9.9.1)(typescript@5.5.4))(eslint@9.9.1)(typescript@5.5.4)': + '@typescript-eslint/eslint-plugin@8.55.0(@typescript-eslint/parser@8.55.0(eslint@10.0.0)(typescript@5.5.4))(eslint@10.0.0)(typescript@5.5.4)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.48.1(eslint@9.9.1)(typescript@5.5.4) - '@typescript-eslint/scope-manager': 8.48.1 - '@typescript-eslint/type-utils': 8.48.1(eslint@9.9.1)(typescript@5.5.4) - '@typescript-eslint/utils': 8.48.1(eslint@9.9.1)(typescript@5.5.4) - '@typescript-eslint/visitor-keys': 8.48.1 - eslint: 9.9.1 - graphemer: 1.4.0 + '@typescript-eslint/parser': 8.55.0(eslint@10.0.0)(typescript@5.5.4) + '@typescript-eslint/scope-manager': 8.55.0 + '@typescript-eslint/type-utils': 8.55.0(eslint@10.0.0)(typescript@5.5.4) + '@typescript-eslint/utils': 8.55.0(eslint@10.0.0)(typescript@5.5.4) + '@typescript-eslint/visitor-keys': 8.55.0 + eslint: 10.0.0 ignore: 7.0.5 natural-compare: 1.4.0 - ts-api-utils: 2.1.0(typescript@5.5.4) + ts-api-utils: 2.4.0(typescript@5.5.4) typescript: 5.5.4 transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.48.1(eslint@9.9.1)(typescript@5.5.4)': + '@typescript-eslint/parser@8.55.0(eslint@10.0.0)(typescript@5.5.4)': dependencies: - '@typescript-eslint/scope-manager': 8.48.1 - '@typescript-eslint/types': 8.48.1 - '@typescript-eslint/typescript-estree': 8.48.1(typescript@5.5.4) - '@typescript-eslint/visitor-keys': 8.48.1 + '@typescript-eslint/scope-manager': 8.55.0 + '@typescript-eslint/types': 8.55.0 + '@typescript-eslint/typescript-estree': 8.55.0(typescript@5.5.4) + '@typescript-eslint/visitor-keys': 8.55.0 debug: 4.4.3 - eslint: 9.9.1 + eslint: 10.0.0 typescript: 5.5.4 transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.48.1(typescript@5.5.4)': + '@typescript-eslint/project-service@8.55.0(typescript@5.5.4)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.48.1(typescript@5.5.4) - '@typescript-eslint/types': 8.48.1 + '@typescript-eslint/tsconfig-utils': 8.55.0(typescript@5.5.4) + '@typescript-eslint/types': 8.55.0 debug: 4.4.3 typescript: 5.5.4 transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@8.48.1': + '@typescript-eslint/scope-manager@8.55.0': dependencies: - '@typescript-eslint/types': 8.48.1 - '@typescript-eslint/visitor-keys': 8.48.1 + '@typescript-eslint/types': 8.55.0 + '@typescript-eslint/visitor-keys': 8.55.0 - '@typescript-eslint/tsconfig-utils@8.48.1(typescript@5.5.4)': + '@typescript-eslint/tsconfig-utils@8.55.0(typescript@5.5.4)': dependencies: typescript: 5.5.4 - '@typescript-eslint/type-utils@8.48.1(eslint@9.9.1)(typescript@5.5.4)': + '@typescript-eslint/type-utils@8.55.0(eslint@10.0.0)(typescript@5.5.4)': dependencies: - '@typescript-eslint/types': 8.48.1 - '@typescript-eslint/typescript-estree': 8.48.1(typescript@5.5.4) - '@typescript-eslint/utils': 8.48.1(eslint@9.9.1)(typescript@5.5.4) + '@typescript-eslint/types': 8.55.0 + '@typescript-eslint/typescript-estree': 8.55.0(typescript@5.5.4) + '@typescript-eslint/utils': 8.55.0(eslint@10.0.0)(typescript@5.5.4) debug: 4.4.3 - eslint: 9.9.1 - ts-api-utils: 2.1.0(typescript@5.5.4) + eslint: 10.0.0 + ts-api-utils: 2.4.0(typescript@5.5.4) typescript: 5.5.4 transitivePeerDependencies: - supports-color - '@typescript-eslint/types@8.48.1': {} + '@typescript-eslint/types@8.55.0': {} - '@typescript-eslint/typescript-estree@8.48.1(typescript@5.5.4)': + '@typescript-eslint/typescript-estree@8.55.0(typescript@5.5.4)': dependencies: - '@typescript-eslint/project-service': 8.48.1(typescript@5.5.4) - '@typescript-eslint/tsconfig-utils': 8.48.1(typescript@5.5.4) - '@typescript-eslint/types': 8.48.1 - '@typescript-eslint/visitor-keys': 8.48.1 + '@typescript-eslint/project-service': 8.55.0(typescript@5.5.4) + '@typescript-eslint/tsconfig-utils': 8.55.0(typescript@5.5.4) + '@typescript-eslint/types': 8.55.0 + '@typescript-eslint/visitor-keys': 8.55.0 debug: 4.4.3 minimatch: 9.0.5 semver: 7.7.3 tinyglobby: 0.2.15 - ts-api-utils: 2.1.0(typescript@5.5.4) + ts-api-utils: 2.4.0(typescript@5.5.4) typescript: 5.5.4 transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.48.1(eslint@9.9.1)(typescript@5.5.4)': + '@typescript-eslint/utils@8.55.0(eslint@10.0.0)(typescript@5.5.4)': dependencies: - '@eslint-community/eslint-utils': 4.9.0(eslint@9.9.1) - '@typescript-eslint/scope-manager': 8.48.1 - '@typescript-eslint/types': 8.48.1 - '@typescript-eslint/typescript-estree': 8.48.1(typescript@5.5.4) - eslint: 9.9.1 + '@eslint-community/eslint-utils': 4.9.1(eslint@10.0.0) + '@typescript-eslint/scope-manager': 8.55.0 + '@typescript-eslint/types': 8.55.0 + '@typescript-eslint/typescript-estree': 8.55.0(typescript@5.5.4) + eslint: 10.0.0 typescript: 5.5.4 transitivePeerDependencies: - supports-color - '@typescript-eslint/visitor-keys@8.48.1': + '@typescript-eslint/visitor-keys@8.55.0': dependencies: - '@typescript-eslint/types': 8.48.1 + '@typescript-eslint/types': 8.55.0 eslint-visitor-keys: 4.2.1 '@vitest/coverage-v8@2.1.9(vitest@2.1.9(@types/node@20.19.17)(jsdom@25.0.1)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0))': @@ -3380,11 +3404,6 @@ snapshots: birpc@2.5.0: {} - brace-expansion@1.1.12: - dependencies: - balanced-match: 1.0.2 - concat-map: 0.0.1 - brace-expansion@2.0.2: dependencies: balanced-match: 1.0.2 @@ -3401,8 +3420,6 @@ snapshots: cac@6.7.14: {} - callsites@3.1.0: {} - chai@5.3.3: dependencies: assertion-error: 2.0.1 @@ -3411,11 +3428,6 @@ snapshots: loupe: 3.2.1 pathval: 2.0.0 - chalk@4.1.2: - dependencies: - ansi-styles: 4.3.0 - supports-color: 7.2.0 - chardet@2.1.0: {} check-error@2.1.1: {} @@ -3451,8 +3463,6 @@ snapshots: commondir@1.0.1: {} - concat-map@0.0.1: {} - cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -3500,7 +3510,7 @@ snapshots: detect-libc@1.0.3: optional: true - devalue@5.5.0: {} + devalue@5.6.2: {} dir-glob@3.0.1: dependencies: @@ -3599,31 +3609,33 @@ snapshots: escape-string-regexp@4.0.0: {} - eslint-compat-utils@0.5.1(eslint@9.9.1): + eslint-compat-utils@0.5.1(eslint@10.0.0): dependencies: - eslint: 9.9.1 + eslint: 10.0.0 semver: 7.7.3 - eslint-config-prettier@9.1.0(eslint@9.9.1): + eslint-config-prettier@9.1.0(eslint@10.0.0): dependencies: - eslint: 9.9.1 + eslint: 10.0.0 - eslint-plugin-es-x@7.8.0(eslint@9.9.1): + eslint-plugin-es-x@7.8.0(eslint@10.0.0): dependencies: - '@eslint-community/eslint-utils': 4.9.0(eslint@9.9.1) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.0.0) '@eslint-community/regexpp': 4.12.2 - eslint: 9.9.1 - eslint-compat-utils: 0.5.1(eslint@9.9.1) + eslint: 10.0.0 + eslint-compat-utils: 0.5.1(eslint@10.0.0) - eslint-plugin-lube@0.4.3: {} + eslint-plugin-lube@0.5.1(eslint@10.0.0): + dependencies: + eslint: 10.0.0 - eslint-plugin-n@17.16.1(eslint@9.9.1)(typescript@5.5.4): + eslint-plugin-n@17.16.1(eslint@10.0.0)(typescript@5.5.4): dependencies: - '@eslint-community/eslint-utils': 4.9.0(eslint@9.9.1) - '@typescript-eslint/utils': 8.48.1(eslint@9.9.1)(typescript@5.5.4) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.0.0) + '@typescript-eslint/utils': 8.55.0(eslint@10.0.0)(typescript@5.5.4) enhanced-resolve: 5.18.3 - eslint: 9.9.1 - eslint-plugin-es-x: 7.8.0(eslint@9.9.1) + eslint: 10.0.0 + eslint-plugin-es-x: 7.8.0(eslint@10.0.0) get-tsconfig: 4.13.0 globals: 15.15.0 ignore: 5.3.2 @@ -3634,11 +3646,11 @@ snapshots: - supports-color - typescript - eslint-plugin-svelte@3.11.0(eslint@9.9.1)(svelte@packages+svelte): + eslint-plugin-svelte@3.15.0(eslint@10.0.0)(svelte@packages+svelte): dependencies: - '@eslint-community/eslint-utils': 4.9.0(eslint@9.9.1) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.0.0) '@jridgewell/sourcemap-codec': 1.5.0 - eslint: 9.9.1 + eslint: 10.0.0 esutils: 2.0.3 globals: 16.3.0 known-css-properties: 0.37.0 @@ -3646,7 +3658,7 @@ snapshots: postcss-load-config: 3.1.4(postcss@8.5.6) postcss-safe-parser: 7.0.1(postcss@8.5.6) semver: 7.7.3 - svelte-eslint-parser: 1.3.0(svelte@packages+svelte) + svelte-eslint-parser: 1.4.1(svelte@packages+svelte) optionalDependencies: svelte: link:packages/svelte transitivePeerDependencies: @@ -3657,29 +3669,39 @@ snapshots: esrecurse: 4.3.0 estraverse: 5.3.0 + eslint-scope@9.1.0: + dependencies: + '@types/esrecurse': 4.3.1 + '@types/estree': 1.0.8 + esrecurse: 4.3.0 + estraverse: 5.3.0 + eslint-visitor-keys@3.4.3: {} eslint-visitor-keys@4.2.1: {} - eslint@9.9.1: + eslint-visitor-keys@5.0.0: {} + + eslint@10.0.0: dependencies: - '@eslint-community/eslint-utils': 4.9.0(eslint@9.9.1) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.0.0) '@eslint-community/regexpp': 4.12.2 - '@eslint/config-array': 0.18.0 - '@eslint/eslintrc': 3.1.0 - '@eslint/js': 9.9.1 + '@eslint/config-array': 0.23.1 + '@eslint/config-helpers': 0.5.2 + '@eslint/core': 1.1.0 + '@eslint/plugin-kit': 0.6.0 + '@humanfs/node': 0.16.7 '@humanwhocodes/module-importer': 1.0.1 - '@humanwhocodes/retry': 0.3.0 - '@nodelib/fs.walk': 1.2.8 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.8 ajv: 6.12.6 - chalk: 4.1.2 cross-spawn: 7.0.6 debug: 4.4.3 escape-string-regexp: 4.0.0 - eslint-scope: 8.4.0 - eslint-visitor-keys: 4.2.1 - espree: 10.1.0 - esquery: 1.5.0 + eslint-scope: 9.1.0 + eslint-visitor-keys: 5.0.0 + espree: 11.1.0 + esquery: 1.7.0 esutils: 2.0.3 fast-deep-equal: 3.1.3 file-entry-cache: 8.0.0 @@ -3688,15 +3710,10 @@ snapshots: ignore: 5.3.2 imurmurhash: 0.1.4 is-glob: 4.0.3 - is-path-inside: 3.0.3 json-stable-stringify-without-jsonify: 1.0.1 - levn: 0.4.1 - lodash.merge: 4.6.2 - minimatch: 3.1.2 + minimatch: 10.1.2 natural-compare: 1.4.0 optionator: 0.9.3 - strip-ansi: 6.0.1 - text-table: 0.2.0 transitivePeerDependencies: - supports-color @@ -3708,6 +3725,12 @@ snapshots: acorn-jsx: 5.3.2(acorn@8.15.0) eslint-visitor-keys: 4.2.1 + espree@11.1.0: + dependencies: + acorn: 8.15.0 + acorn-jsx: 5.3.2(acorn@8.15.0) + eslint-visitor-keys: 5.0.0 + espree@9.6.1: dependencies: acorn: 8.15.0 @@ -3716,11 +3739,11 @@ snapshots: esprima@4.0.1: {} - esquery@1.5.0: + esquery@1.7.0: dependencies: estraverse: 5.3.0 - esrap@2.2.1: + esrap@2.2.2: dependencies: '@jridgewell/sourcemap-codec': 1.5.0 @@ -3841,12 +3864,12 @@ snapshots: package-json-from-dist: 1.0.0 path-scurry: 1.11.1 - globals@14.0.0: {} - globals@15.15.0: {} globals@16.3.0: {} + globals@17.3.0: {} + globby@11.1.0: dependencies: array-union: 2.1.0 @@ -3858,8 +3881,6 @@ snapshots: graceful-fs@4.2.11: {} - graphemer@1.4.0: {} - has-flag@4.0.0: {} hasown@2.0.0: @@ -3903,11 +3924,6 @@ snapshots: immutable@4.3.7: optional: true - import-fresh@3.3.0: - dependencies: - parent-module: 1.0.1 - resolve-from: 4.0.0 - imurmurhash@0.1.4: {} is-binary-path@2.1.0: @@ -3937,8 +3953,6 @@ snapshots: is-number@7.0.0: {} - is-path-inside@3.0.3: {} - is-potential-custom-element-name@1.0.1: {} is-reference@1.2.1: @@ -4102,8 +4116,6 @@ snapshots: dependencies: p-locate: 5.0.0 - lodash.merge@4.6.2: {} - lodash.startcase@4.4.0: {} loupe@3.2.1: {} @@ -4137,9 +4149,9 @@ snapshots: dependencies: mime-db: 1.52.0 - minimatch@3.1.2: + minimatch@10.1.2: dependencies: - brace-expansion: 1.1.12 + '@isaacs/brace-expansion': 5.0.1 minimatch@9.0.5: dependencies: @@ -4216,10 +4228,6 @@ snapshots: dependencies: quansync: 0.2.11 - parent-module@1.0.1: - dependencies: - callsites: 3.1.0 - parse5@7.1.2: dependencies: entities: 4.5.0 @@ -4253,11 +4261,11 @@ snapshots: pify@4.0.1: {} - playwright-core@1.46.1: {} + playwright-core@1.58.0: {} - playwright@1.46.1: + playwright@1.58.0: dependencies: - playwright-core: 1.46.1 + playwright-core: 1.58.0 optionalDependencies: fsevents: 2.3.2 @@ -4327,8 +4335,6 @@ snapshots: regexparam@3.0.0: {} - resolve-from@4.0.0: {} - resolve-from@5.0.0: {} resolve-pkg-maps@1.0.0: {} @@ -4466,15 +4472,13 @@ snapshots: strip-bom@3.0.0: {} - strip-json-comments@3.1.1: {} - supports-color@7.2.0: dependencies: has-flag: 4.0.0 supports-preserve-symlinks-flag@1.0.0: {} - svelte-eslint-parser@1.3.0(svelte@packages+svelte): + svelte-eslint-parser@1.4.1(svelte@packages+svelte): dependencies: eslint-scope: 8.4.0 eslint-visitor-keys: 4.2.1 @@ -4504,8 +4508,6 @@ snapshots: glob: 10.5.0 minimatch: 9.0.5 - text-table@0.2.0: {} - tinybench@2.9.0: {} tinyexec@0.3.2: {} @@ -4553,7 +4555,7 @@ snapshots: dependencies: typescript: 5.5.4 - ts-api-utils@2.1.0(typescript@5.5.4): + ts-api-utils@2.4.0(typescript@5.5.4): dependencies: typescript: 5.5.4 @@ -4566,13 +4568,13 @@ snapshots: dependencies: prelude-ls: 1.2.1 - typescript-eslint@8.48.1(eslint@9.9.1)(typescript@5.5.4): + typescript-eslint@8.55.0(eslint@10.0.0)(typescript@5.5.4): dependencies: - '@typescript-eslint/eslint-plugin': 8.48.1(@typescript-eslint/parser@8.48.1(eslint@9.9.1)(typescript@5.5.4))(eslint@9.9.1)(typescript@5.5.4) - '@typescript-eslint/parser': 8.48.1(eslint@9.9.1)(typescript@5.5.4) - '@typescript-eslint/typescript-estree': 8.48.1(typescript@5.5.4) - '@typescript-eslint/utils': 8.48.1(eslint@9.9.1)(typescript@5.5.4) - eslint: 9.9.1 + '@typescript-eslint/eslint-plugin': 8.55.0(@typescript-eslint/parser@8.55.0(eslint@10.0.0)(typescript@5.5.4))(eslint@10.0.0)(typescript@5.5.4) + '@typescript-eslint/parser': 8.55.0(eslint@10.0.0)(typescript@5.5.4) + '@typescript-eslint/typescript-estree': 8.55.0(typescript@5.5.4) + '@typescript-eslint/utils': 8.55.0(eslint@10.0.0)(typescript@5.5.4) + eslint: 10.0.0 typescript: 5.5.4 transitivePeerDependencies: - supports-color diff --git a/vitest-xhtml-environment.ts b/vitest-xhtml-environment.ts new file mode 100644 index 0000000000..dc8b271440 --- /dev/null +++ b/vitest-xhtml-environment.ts @@ -0,0 +1,19 @@ +import { type Environment, builtinEnvironments } from 'vitest/environments'; + +const xhtml_page = ` + +`; + +export default { + name: 'jsdom-xhtml', + transformMode: 'web', + setup(global, { jsdom = {} }) { + return builtinEnvironments.jsdom.setup(global, { + jsdom: { + ...jsdom, + html: xhtml_page, + contentType: 'application/xhtml+xml' + } + }); + } +};