diff --git a/.changeset/loose-sloths-guess.md b/.changeset/loose-sloths-guess.md new file mode 100644 index 0000000000..450040349d --- /dev/null +++ b/.changeset/loose-sloths-guess.md @@ -0,0 +1,5 @@ +--- +'svelte': minor +--- + +feat: allow passing `ShadowRootInit` object to custom element `shadow` option diff --git a/.changeset/mighty-mice-call.md b/.changeset/mighty-mice-call.md deleted file mode 100644 index 340b33bd4b..0000000000 --- a/.changeset/mighty-mice-call.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: keep batches alive until all async work is complete diff --git a/.changeset/short-banks-yell.md b/.changeset/short-banks-yell.md deleted file mode 100644 index 34d5ba66d3..0000000000 --- a/.changeset/short-banks-yell.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: don't preserve reactivity context across function boundaries diff --git a/.changeset/silly-penguins-sleep.md b/.changeset/silly-penguins-sleep.md deleted file mode 100644 index f397f1e8ba..0000000000 --- a/.changeset/silly-penguins-sleep.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: make `$inspect` logs come from the callsite diff --git a/.changeset/witty-seas-learn.md b/.changeset/witty-seas-learn.md deleted file mode 100644 index aa94c7c35f..0000000000 --- a/.changeset/witty-seas-learn.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: ensure guards (eg. if, each, key) run before their contents diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 51408fc8cc..8dcf1e45dc 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,9 +82,9 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 5 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - uses: pnpm/action-setup@v4 - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v6 with: node-version: 18 cache: pnpm @@ -103,9 +103,9 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 15 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - uses: pnpm/action-setup@v4 - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v6 with: node-version: 18 cache: pnpm diff --git a/.github/workflows/pkg.pr.new-comment.yml b/.github/workflows/pkg.pr.new-comment.yml index 8bf13bb8f1..64495cc5c8 100644 --- a/.github/workflows/pkg.pr.new-comment.yml +++ b/.github/workflows/pkg.pr.new-comment.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Download artifact - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v7 with: name: output github-token: ${{ secrets.GITHUB_TOKEN }} @@ -23,7 +23,7 @@ jobs: - run: ls -R . - name: 'Post or update comment' - uses: actions/github-script@v6 + uses: actions/github-script@v8 with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | diff --git a/.github/workflows/pkg.pr.new.yml b/.github/workflows/pkg.pr.new.yml index 49303f1684..252cbed769 100644 --- a/.github/workflows/pkg.pr.new.yml +++ b/.github/workflows/pkg.pr.new.yml @@ -10,9 +10,9 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - uses: pnpm/action-setup@v4 - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v6 with: node-version: 22.x cache: pnpm @@ -25,7 +25,7 @@ jobs: - run: pnpx pkg-pr-new publish --comment=off --json output.json --compact --no-template './packages/svelte' - name: Add metadata to output - uses: actions/github-script@v6 + uses: actions/github-script@v8 with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | @@ -36,7 +36,7 @@ jobs: output.ref = context.ref; fs.writeFileSync('output.json', JSON.stringify(output), 'utf8'); - name: Upload output - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: output path: ./output.json 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/.prettierignore b/.prettierignore index 9cf9a4bfe1..ee5ef6d8c6 100644 --- a/.prettierignore +++ b/.prettierignore @@ -25,6 +25,10 @@ packages/svelte/tests/**/_output packages/svelte/tests/**/shards/*.test.js packages/svelte/tests/hydration/samples/*/_expected.html packages/svelte/tests/hydration/samples/*/_override.html +packages/svelte/tests/parser-legacy/samples/*/_actual.json +packages/svelte/tests/parser-legacy/samples/*/output.json +packages/svelte/tests/parser-modern/samples/*/_actual.json +packages/svelte/tests/parser-modern/samples/*/output.json packages/svelte/types packages/svelte/compiler/index.js playgrounds/sandbox/src/* 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 2ad22c8469..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). @@ -23,9 +23,10 @@ There are also [plugins for other bundlers](/packages#bundler-plugins), but we r ## Editor tooling -The Svelte team maintains a [VS Code extension](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode), and there are integrations with various other [editors](https://sveltesociety.dev/resources#editor-support) and tools as well. +The Svelte team maintains a [VS Code extension](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode), and there are integrations with various other [editors](https://sveltesociety.dev/collection/editor-support-c85c080efc292a34) and tools as well. + +You can also check your code from the command line using [`npx sv check`](https://svelte.dev/docs/cli/sv-check). -You can also check your code from the command line using [sv check](https://github.com/sveltejs/cli). ## Getting help diff --git a/documentation/docs/02-runes/05-$props.md b/documentation/docs/02-runes/05-$props.md index 222b4831b6..bd5ae16853 100644 --- a/documentation/docs/02-runes/05-$props.md +++ b/documentation/docs/02-runes/05-$props.md @@ -198,6 +198,8 @@ You can, of course, separate the type declaration from the annotation: > [!NOTE] Interfaces for native DOM elements are provided in the `svelte/elements` module (see [Typing wrapper components](typescript#Typing-wrapper-components)) +If your component exposes [snippet](snippet) props like `children`, these should be typed using the `Snippet` interface imported from `'svelte'` — see [Typing snippets](snippet#Typing-snippets) for examples. + Adding types is recommended, as it ensures that people using your component can easily discover which props they should provide. diff --git a/documentation/docs/02-runes/06-$bindable.md b/documentation/docs/02-runes/06-$bindable.md index c12c2bf490..3675a56b16 100644 --- a/documentation/docs/02-runes/06-$bindable.md +++ b/documentation/docs/02-runes/06-$bindable.md @@ -4,7 +4,7 @@ title: $bindable Ordinarily, props go one way, from parent to child. This makes it easy to understand how data flows around your app. -In Svelte, component props can be _bound_, which means that data can also flow _up_ from child to parent. This isn't something you should do often, but it can simplify your code if used sparingly and carefully. +In Svelte, component props can be _bound_, which means that data can also flow _up_ from child to parent. This isn't something you should do often — overuse can make your data flow unpredictable and your components harder to maintain — but it can simplify your code if used sparingly and carefully. It also means that a state proxy can be _mutated_ in the child. diff --git a/documentation/docs/03-template-syntax/03-each.md b/documentation/docs/03-template-syntax/03-each.md index 006cadd152..57ed0def71 100644 --- a/documentation/docs/03-template-syntax/03-each.md +++ b/documentation/docs/03-template-syntax/03-each.md @@ -12,7 +12,9 @@ title: {#each ...} {#each expression as name, index}...{/each} ``` -Iterating over values can be done with an each block. The values in question can be arrays, array-like objects (i.e. anything with a `length` property), or iterables like `Map` and `Set` — in other words, anything that can be used with `Array.from`. +Iterating over values can be done with an each block. The values in question can be arrays, array-like objects (i.e. anything with a `length` property), or iterables like `Map` and `Set`— in other words, anything that can be used with `Array.from`. + +If the value is `null` or `undefined`, it is treated the same as an empty array (which will cause [else blocks](#Else-blocks) to be rendered, where applicable). ```svelte

Shopping list

diff --git a/documentation/docs/03-template-syntax/19-await-expressions.md b/documentation/docs/03-template-syntax/19-await-expressions.md index 1c613af870..2f73f6a47c 100644 --- a/documentation/docs/03-template-syntax/19-await-expressions.md +++ b/documentation/docs/03-template-syntax/19-await-expressions.md @@ -135,6 +135,54 @@ If a `` with a `pending` snippet is encountered during SSR, tha > [!NOTE] In the future, we plan to add a streaming implementation that renders the content in the background. +## Forking + +The [`fork(...)`](svelte#fork) API, added in 5.42, makes it possible to run `await` expressions that you _expect_ to happen in the near future. This is mainly intended for frameworks like SvelteKit to implement preloading when (for example) users signal an intent to navigate. + +```svelte + + + + +{#if open} + + open = false} /> +{/if} +``` + ## Caveats As an experimental feature, the details of how `await` is handled (and related APIs like `$effect.pending()`) are subject to breaking changes outside of a semver major release, though we intend to keep such changes to a bare minimum. diff --git a/documentation/docs/06-runtime/03-lifecycle-hooks.md b/documentation/docs/06-runtime/03-lifecycle-hooks.md index f7a78beec9..95e1c260c1 100644 --- a/documentation/docs/06-runtime/03-lifecycle-hooks.md +++ b/documentation/docs/06-runtime/03-lifecycle-hooks.md @@ -94,7 +94,7 @@ Svelte 4 contained hooks that ran before and after the component as a whole was ``` -Instead of `beforeUpdate` use `$effect.pre` and instead of `afterUpdate` use `$effect` instead - these runes offer more granular control and only react to the changes you're actually interested in. +Instead of `beforeUpdate` use `$effect.pre` and instead of `afterUpdate` use `$effect` instead — these runes offer more granular control and only react to the changes you're actually interested in. ### Chat window example diff --git a/documentation/docs/06-runtime/05-hydratable.md b/documentation/docs/06-runtime/05-hydratable.md new file mode 100644 index 0000000000..f8d5130581 --- /dev/null +++ b/documentation/docs/06-runtime/05-hydratable.md @@ -0,0 +1,123 @@ +--- +title: Hydratable data +--- + +In Svelte, when you want to render asynchronous content data on the server, you can simply `await` it. This is great! However, it comes with a pitfall: when hydrating that content on the client, Svelte has to redo the asynchronous work, which blocks hydration for however long it takes: + +```svelte + + +

{user.name}

+``` + +That's silly, though. If we've already done the hard work of getting the data on the server, we don't want to get it again during hydration on the client. `hydratable` is a low-level API built to solve this problem. You probably won't need this very often — it will be used behind the scenes by whatever datafetching library you use. For example, it powers [remote functions in SvelteKit](/docs/kit/remote-functions). + +To fix the example above: + +```svelte + + +

{user.name}

+``` + +This API can also be used to provide access to random or time-based values that are stable between server rendering and hydration. For example, to get a random number that doesn't update on hydration: + +```ts +import { hydratable } from 'svelte'; +const rand = hydratable('random', () => Math.random()); +``` + +If you're a library author, be sure to prefix the keys of your `hydratable` values with the name of your library so that your keys don't conflict with other libraries. + +## Serialization + +All data returned from a `hydratable` function must be serializable. But this doesn't mean you're limited to JSON — Svelte uses [`devalue`](https://npmjs.com/package/devalue), which can serialize all sorts of things including `Map`, `Set`, `URL`, and `BigInt`. Check the documentation page for a full list. In addition to these, thanks to some Svelte magic, you can also fearlessly use promises: + +```svelte + + +{await promises.one} +{await promises.two} +``` + +## CSP + +`hydratable` adds an inline ` +``` + ### hydration_failed ``` diff --git a/documentation/docs/98-reference/.generated/client-warnings.md b/documentation/docs/98-reference/.generated/client-warnings.md index c95ace2229..7daf808d61 100644 --- a/documentation/docs/98-reference/.generated/client-warnings.md +++ b/documentation/docs/98-reference/.generated/client-warnings.md @@ -140,6 +140,25 @@ The easiest way to log a value as it changes over time is to use the [`$inspect` %handler% should be a function. Did you mean to %suggestion%? ``` +### hydratable_missing_but_expected + +``` +Expected to find a hydratable with key `%key%` during hydration, but did not. +``` + +This can happen if you render a hydratable on the client that was not rendered on the server, and means that it was forced to fall back to running its function blockingly during hydration. This is bad for performance, as it blocks hydration until the asynchronous work completes. + +```svelte + +``` + ### hydration_attribute_changed ``` @@ -218,7 +237,7 @@ Hydration failed because the initial UI does not match what was rendered on the This warning is thrown when Svelte encounters an error while hydrating the HTML from the server. During hydration, Svelte walks the DOM, expecting a certain structure. If that structure is different (for example because the HTML was repaired by the DOM because of invalid HTML), then Svelte will run into issues, resulting in this warning. -During development, this error is often preceeded by a `console.error` detailing the offending HTML, which needs fixing. +During development, this error is often preceded by a `console.error` detailing the offending HTML, which needs fixing. ### invalid_raw_snippet_render diff --git a/documentation/docs/98-reference/.generated/compile-errors.md b/documentation/docs/98-reference/.generated/compile-errors.md index c5703c636b..079b1d2a0a 100644 --- a/documentation/docs/98-reference/.generated/compile-errors.md +++ b/documentation/docs/98-reference/.generated/compile-errors.md @@ -193,7 +193,7 @@ Cyclical dependency detected: %cycle% ### const_tag_invalid_placement ``` -`{@const}` must be the immediate child of `{#snippet}`, `{#if}`, `{:else if}`, `{:else}`, `{#each}`, `{:then}`, `{:catch}`, ``, `` +`{@const}` must be the immediate child of `{#snippet}`, `{#if}`, `{:else if}`, `{:else}`, `{#each}`, `{:then}`, `{:catch}`, ``, `` or `` ``` ### const_tag_invalid_reference @@ -525,6 +525,12 @@ Expected an identifier Expected identifier or destructure pattern ``` +### expected_tag + +``` +Expected 'html', 'render', 'attach', 'const', or 'debug' +``` + ### expected_token ``` @@ -561,6 +567,12 @@ Cannot use `await` in deriveds and template expressions, or at the top level of `$host()` can only be used inside custom element component instances ``` +### illegal_await_expression + +``` +`use:`, `transition:` and `animate:` directives, attachments and bindings do not support await expressions +``` + ### illegal_element_attribute ``` @@ -1090,7 +1102,7 @@ Value must be %list%, if specified ### svelte_options_invalid_customelement ``` -"customElement" must be a string literal defining a valid custom element name or an object of the form { tag?: string; shadow?: "open" | "none"; props?: { [key: string]: { attribute?: string; reflect?: boolean; type: .. } } } +"customElement" must be a string literal defining a valid custom element name or an object of the form { tag?: string; shadow?: "open" | "none" | `ShadowRootInit`; props?: { [key: string]: { attribute?: string; reflect?: boolean; type: .. } } } ``` ### svelte_options_invalid_customelement_props @@ -1102,9 +1114,11 @@ Value must be %list%, if specified ### svelte_options_invalid_customelement_shadow ``` -"shadow" must be either "open" or "none" +"shadow" must be either "open", "none" or `ShadowRootInit` object. ``` +See https://developer.mozilla.org/en-US/docs/Web/API/Element/attachShadow#options for more information on valid shadow root constructor options + ### svelte_options_invalid_tagname ``` diff --git a/documentation/docs/98-reference/.generated/server-errors.md b/documentation/docs/98-reference/.generated/server-errors.md index 6263032212..c98756afec 100644 --- a/documentation/docs/98-reference/.generated/server-errors.md +++ b/documentation/docs/98-reference/.generated/server-errors.md @@ -1,5 +1,13 @@ +### async_local_storage_unavailable + +``` +The node API `AsyncLocalStorage` is not available, but is required to use async server rendering. +``` + +Some platforms require configuration flags to enable this API. Consult your platform's documentation. + ### await_invalid ``` @@ -14,6 +22,45 @@ You (or the framework you're using) called [`render(...)`](svelte-server#render) The `html` property of server render results has been deprecated. Use `body` instead. ``` +### hydratable_clobbering + +``` +Attempted to set `hydratable` with key `%key%` twice with different values. + +%stack% +``` + +This error occurs when using `hydratable` multiple times with the same key. To avoid this, you can: +- Ensure all invocations with the same key result in the same value +- Update the keys to make both instances unique + +```svelte + +``` + +### hydratable_serialization_failed + +``` +Failed to serialize `hydratable` data for key `%key%`. + +`hydratable` can serialize anything [`uneval` from `devalue`](https://npmjs.com/package/uneval) can, plus Promises. + +Cause: +%stack% +``` + +### invalid_csp + +``` +`csp.nonce` was set while `csp.hash` was `true`. These options cannot be used simultaneously. +``` + ### lifecycle_function_unavailable ``` @@ -21,3 +68,11 @@ The `html` property of server render results has been deprecated. Use `body` ins ``` Certain methods such as `mount` cannot be invoked while running in a server context. Avoid calling them eagerly, i.e. not during render. + +### server_context_required + +``` +Could not resolve `render` context. +``` + +Certain functions such as `hydratable` cannot be invoked outside of a `render(...)` call, such as at the top level of a module. diff --git a/documentation/docs/98-reference/.generated/server-warnings.md b/documentation/docs/98-reference/.generated/server-warnings.md new file mode 100644 index 0000000000..c4a7fbefef --- /dev/null +++ b/documentation/docs/98-reference/.generated/server-warnings.md @@ -0,0 +1,34 @@ + + +### unresolved_hydratable + +``` +A `hydratable` value with key `%key%` was created, but at least part of it was not used during the render. + +The `hydratable` was initialized in: +%stack% +``` + +The most likely cause of this is creating a `hydratable` in the `script` block of your component and then `await`ing +the result inside a `svelte:boundary` with a `pending` snippet: + +```svelte + + + +

{(await user).name}

+ + {#snippet pending()} +
Loading...
+ {/snippet} +
+``` + +Consider inlining the `hydratable` call inside the boundary so that it's not called on the server. + +Note that this can also happen when a `hydratable` contains multiple promises and some but not all of them have been used. diff --git a/documentation/docs/98-reference/.generated/shared-errors.md b/documentation/docs/98-reference/.generated/shared-errors.md index 07e13dea45..136b3f4957 100644 --- a/documentation/docs/98-reference/.generated/shared-errors.md +++ b/documentation/docs/98-reference/.generated/shared-errors.md @@ -1,5 +1,11 @@ +### experimental_async_required + +``` +Cannot use `%name%(...)` unless the `experimental.async` compiler option is `true` +``` + ### invalid_default_snippet ``` diff --git a/documentation/docs/99-legacy/10-legacy-on.md b/documentation/docs/99-legacy/10-legacy-on.md index f2ee694cc1..ba084a228b 100644 --- a/documentation/docs/99-legacy/10-legacy-on.md +++ b/documentation/docs/99-legacy/10-legacy-on.md @@ -43,7 +43,7 @@ The following modifiers are available: - `preventDefault` — calls `event.preventDefault()` before running the handler - `stopPropagation` — calls `event.stopPropagation()`, preventing the event reaching the next element -- `stopImmediatePropagation` - calls `event.stopImmediatePropagation()`, preventing other listeners of the same event from being fired. +- `stopImmediatePropagation` — calls `event.stopImmediatePropagation()`, preventing other listeners of the same event from being fired. - `passive` — improves scrolling performance on touch/wheel events (Svelte will add it automatically where it's safe to do so) - `nonpassive` — explicitly set `passive: false` - `capture` — fires the handler during the _capture_ phase instead of the _bubbling_ phase diff --git a/package.json b/package.json index ad60494bf2..24be8bd2bc 100644 --- a/package.json +++ b/package.json @@ -21,12 +21,12 @@ "test": "vitest run", "changeset:version": "changeset version && pnpm -r generate:version && git add --all", "changeset:publish": "changeset publish", - "bench": "node --allow-natives-syntax ./benchmarking/run.js", - "bench:compare": "node --allow-natives-syntax ./benchmarking/compare/index.js", - "bench:debug": "node --allow-natives-syntax --inspect-brk ./benchmarking/run.js" + "bench": "NODE_ENV=production node --allow-natives-syntax ./benchmarking/run.js", + "bench:compare": "NODE_ENV=production node --allow-natives-syntax ./benchmarking/compare/index.js", + "bench:debug": "NODE_ENV=production node --allow-natives-syntax --inspect-brk ./benchmarking/run.js" }, "devDependencies": { - "@changesets/cli": "^2.29.7", + "@changesets/cli": "^2.29.8", "@sveltejs/eslint-config": "^8.3.3", "@svitejs/changesets-changelog-github-compact": "^1.1.0", "@types/node": "^20.11.5", @@ -41,7 +41,7 @@ "prettier-plugin-svelte": "^3.4.0", "svelte": "workspace:^", "typescript": "^5.5.4", - "typescript-eslint": "^8.24.0", + "typescript-eslint": "^8.48.1", "v8-natives": "^1.2.5", "vitest": "^2.1.9" } diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 4db131114d..af503dbb20 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,445 @@ # svelte +## 5.48.5 + +### Patch Changes + +- fix: run boundary `onerror` callbacks in a microtask, in case they result in the boundary's destruction ([#17561](https://github.com/sveltejs/svelte/pull/17561)) + +- fix: prevent unintended exports from namespaces ([#17562](https://github.com/sveltejs/svelte/pull/17562)) + +- fix: each block breaking with effects interspersed among items ([#17550](https://github.com/sveltejs/svelte/pull/17550)) + +## 5.48.4 + +### Patch Changes + +- fix: avoid duplicating escaped characters in CSS AST ([#17554](https://github.com/sveltejs/svelte/pull/17554)) + +## 5.48.3 + +### Patch Changes + +- fix: hydration failing with settled async blocks ([#17539](https://github.com/sveltejs/svelte/pull/17539)) + +- fix: add pointer and touch events to a11y_no_static_element_interactions warning ([#17551](https://github.com/sveltejs/svelte/pull/17551)) + +- fix: handle false dynamic components in SSR ([#17542](https://github.com/sveltejs/svelte/pull/17542)) + +- fix: avoid unnecessary block effect re-runs after async work completes ([#17535](https://github.com/sveltejs/svelte/pull/17535)) + +- fix: avoid using dev-mode array.includes wrapper on internal array checks ([#17536](https://github.com/sveltejs/svelte/pull/17536)) + +## 5.48.2 + +### Patch Changes + +- fix: export `wait` function from internal client index ([#17530](https://github.com/sveltejs/svelte/pull/17530)) + +## 5.48.1 + +### Patch Changes + +- fix: hoist snippets above const in same block ([#17516](https://github.com/sveltejs/svelte/pull/17516)) + +- fix: properly hydrate await in `{@html}` ([#17528](https://github.com/sveltejs/svelte/pull/17528)) + +- fix: batch resolution of async work ([#17511](https://github.com/sveltejs/svelte/pull/17511)) + +- fix: account for empty statements when visiting in transform async ([#17524](https://github.com/sveltejs/svelte/pull/17524)) + +- fix: avoid async overhead for already settled promises ([#17461](https://github.com/sveltejs/svelte/pull/17461)) + +- fix: better code generation for const tags with async dependencies ([#17518](https://github.com/sveltejs/svelte/pull/17518)) + +## 5.48.0 + +### Minor Changes + +- feat: export `parseCss` from `svelte/compiler` ([#17496](https://github.com/sveltejs/svelte/pull/17496)) + +### Patch Changes + +- fix: handle non-string values in `svelte:element` `this` attribute ([#17499](https://github.com/sveltejs/svelte/pull/17499)) + +- fix: faster deduplication of dependencies ([#17503](https://github.com/sveltejs/svelte/pull/17503)) + +## 5.47.1 + +### Patch Changes + +- fix: trigger `selectedcontent` reactivity ([#17486](https://github.com/sveltejs/svelte/pull/17486)) + +## 5.47.0 + +### Minor Changes + +- feat: customizable `, but we assume it here - option: { only: ['#text'] }, + // option or optgroup does not have an `only` restriction because newer browsers support rich HTML content + // inside option elements. For older browsers, hydration will handle the mismatch. form: { descendant: ['form'] }, a: { descendant: ['a'] }, button: { descendant: ['button'] }, @@ -92,8 +92,6 @@ const disallowed_children = { h4: { descendant: ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'] }, h5: { descendant: ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'] }, h6: { descendant: ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'] }, - // https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-inselect - select: { only: ['option', 'optgroup', '#text', 'hr', 'script', 'template'] }, // https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-intd // https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-incaption diff --git a/packages/svelte/src/index-client.js b/packages/svelte/src/index-client.js index 337cbb500b..0eb1b80315 100644 --- a/packages/svelte/src/index-client.js +++ b/packages/svelte/src/index-client.js @@ -241,7 +241,7 @@ function init_update_callbacks(context) { return (l.u ??= { a: [], b: [], m: [] }); } -export { flushSync } from './internal/client/reactivity/batch.js'; +export { flushSync, fork } from './internal/client/reactivity/batch.js'; export { createContext, getContext, @@ -249,6 +249,7 @@ export { hasContext, setContext } from './internal/client/context.js'; +export { hydratable } from './internal/client/hydratable.js'; export { hydrate, mount, unmount } from './internal/client/render.js'; export { tick, untrack, settled } from './internal/client/runtime.js'; export { createRawSnippet } from './internal/client/dom/blocks/snippet.js'; diff --git a/packages/svelte/src/index-server.js b/packages/svelte/src/index-server.js index 223ce6a4cd..9fb810fd9e 100644 --- a/packages/svelte/src/index-server.js +++ b/packages/svelte/src/index-server.js @@ -33,6 +33,10 @@ export function unmount() { e.lifecycle_function_unavailable('unmount'); } +export function fork() { + e.lifecycle_function_unavailable('fork'); +} + export async function tick() {} export async function settled() {} @@ -47,4 +51,6 @@ export { setContext } from './internal/server/context.js'; +export { hydratable } from './internal/server/hydratable.js'; + export { createRawSnippet } from './internal/server/blocks/snippet.js'; diff --git a/packages/svelte/src/index.d.ts b/packages/svelte/src/index.d.ts index 38e6086689..a1782f5b61 100644 --- a/packages/svelte/src/index.d.ts +++ b/packages/svelte/src/index.d.ts @@ -352,4 +352,20 @@ export type MountOptions = Record props: Props; }); +/** + * Represents work that is happening off-screen, such as data being preloaded + * in anticipation of the user navigating + * @since 5.42 + */ +export interface Fork { + /** + * Commit the fork. The promise will resolve once the state change has been applied + */ + commit(): Promise; + /** + * Discard the fork + */ + discard(): void; +} + export * from './index-client.js'; diff --git a/packages/svelte/src/internal/client/constants.js b/packages/svelte/src/internal/client/constants.js index 50a7a21ae8..a1bdb8a985 100644 --- a/packages/svelte/src/internal/client/constants.js +++ b/packages/svelte/src/internal/client/constants.js @@ -1,24 +1,54 @@ +// General flags export const DERIVED = 1 << 1; export const EFFECT = 1 << 2; export const RENDER_EFFECT = 1 << 3; +/** + * An effect that does not destroy its child effects when it reruns. + * Runs as part of render effects, i.e. not eagerly as part of tree traversal or effect flushing. + */ +export const MANAGED_EFFECT = 1 << 24; +/** + * An effect that does not destroy its child effects when it reruns (like MANAGED_EFFECT). + * Runs eagerly as part of tree traversal or effect flushing. + */ export const BLOCK_EFFECT = 1 << 4; export const BRANCH_EFFECT = 1 << 5; export const ROOT_EFFECT = 1 << 6; export const BOUNDARY_EFFECT = 1 << 7; -export const UNOWNED = 1 << 8; -export const DISCONNECTED = 1 << 9; +/** + * Indicates that a reaction is connected to an effect root — either it is an effect, + * or it is a derived that is depended on by at least one effect. If a derived has + * no dependents, we can disconnect it from the graph, allowing it to either be + * GC'd or reconnected later if an effect comes to depend on it again + */ +export const CONNECTED = 1 << 9; export const CLEAN = 1 << 10; export const DIRTY = 1 << 11; export const MAYBE_DIRTY = 1 << 12; export const INERT = 1 << 13; export const DESTROYED = 1 << 14; + +// Flags exclusive to effects +/** Set once an effect that should run synchronously has run */ export const EFFECT_RAN = 1 << 15; -/** 'Transparent' effects do not create a transition boundary */ +/** + * 'Transparent' effects do not create a transition boundary. + * This is on a block effect 99% of the time but may also be on a branch effect if its parent block effect was pruned + */ export const EFFECT_TRANSPARENT = 1 << 16; -export const INSPECT_EFFECT = 1 << 17; +export const EAGER_EFFECT = 1 << 17; export const HEAD_EFFECT = 1 << 18; export const EFFECT_PRESERVED = 1 << 19; export const USER_EFFECT = 1 << 20; +export const EFFECT_OFFSCREEN = 1 << 25; + +// Flags exclusive to deriveds +/** + * Tells that we marked this derived and its reactions as visited during the "mark as (maybe) dirty"-phase. + * Will be lifted during execution of the derived and during checking its dirty state (both are necessary + * because a derived might be checked but not executed). + */ +export const WAS_MARKED = 1 << 15; // Flags used for async export const REACTION_IS_UPDATING = 1 << 21; diff --git a/packages/svelte/src/internal/client/context.js b/packages/svelte/src/internal/client/context.js index 751a35321a..ffdb342adb 100644 --- a/packages/svelte/src/internal/client/context.js +++ b/packages/svelte/src/internal/client/context.js @@ -128,7 +128,11 @@ export function setContext(key, context) { if (async_mode_flag) { var flags = /** @type {Effect} */ (active_effect).f; - var valid = !active_reaction && (flags & BRANCH_EFFECT) !== 0 && (flags & EFFECT_RAN) === 0; + var valid = + !active_reaction && + (flags & BRANCH_EFFECT) !== 0 && + // pop() runs synchronously, so this indicates we're setting context after an await + !(/** @type {ComponentContext} */ (component_context).i); if (!valid) { e.set_context_after_init(); @@ -173,6 +177,7 @@ export function getAllContexts() { export function push(props, runes = false, fn) { component_context = { p: component_context, + i: false, c: null, e: null, s: props, @@ -208,6 +213,8 @@ export function pop(component) { context.x = component; } + context.i = true; + component_context = context.p; if (DEV) { diff --git a/packages/svelte/src/internal/client/dev/debug.js b/packages/svelte/src/internal/client/dev/debug.js index 2714a3af1f..22c4de1179 100644 --- a/packages/svelte/src/internal/client/dev/debug.js +++ b/packages/svelte/src/internal/client/dev/debug.js @@ -5,13 +5,21 @@ import { BOUNDARY_EFFECT, BRANCH_EFFECT, CLEAN, + CONNECTED, DERIVED, + DIRTY, EFFECT, ASYNC, + DESTROYED, + INERT, MAYBE_DIRTY, RENDER_EFFECT, - ROOT_EFFECT + ROOT_EFFECT, + WAS_MARKED, + MANAGED_EFFECT } from '#client/constants'; +import { snapshot } from '../../shared/clone.js'; +import { untrack } from '../runtime.js'; /** * @@ -28,11 +36,13 @@ export function root(effect) { /** * * @param {Effect} effect + * @param {boolean} append_effect + * @returns {string} */ -export function log_effect_tree(effect, depth = 0) { +function effect_label(effect, append_effect = false) { const flags = effect.f; - let label = '(unknown)'; + let label = `(unknown ${append_effect ? 'effect' : ''})`; if ((flags & ROOT_EFFECT) !== 0) { label = 'root'; @@ -40,6 +50,8 @@ export function log_effect_tree(effect, depth = 0) { label = 'boundary'; } else if ((flags & BLOCK_EFFECT) !== 0) { label = 'block'; + } else if ((flags & MANAGED_EFFECT) !== 0) { + label = 'managed'; } else if ((flags & ASYNC) !== 0) { label = 'async'; } else if ((flags & BRANCH_EFFECT) !== 0) { @@ -50,6 +62,20 @@ export function log_effect_tree(effect, depth = 0) { label = 'effect'; } + if (append_effect && !label.endsWith('effect')) { + label += ' effect'; + } + + return label; +} +/** + * + * @param {Effect} effect + */ +export function log_effect_tree(effect, depth = 0) { + const flags = effect.f; + const label = effect_label(effect); + let status = (flags & CLEAN) !== 0 ? 'clean' : (flags & MAYBE_DIRTY) !== 0 ? 'maybe dirty' : 'dirty'; @@ -84,6 +110,16 @@ export function log_effect_tree(effect, depth = 0) { console.groupEnd(); } + if (effect.nodes) { + // eslint-disable-next-line no-console + console.log(effect.nodes.start); + + if (effect.nodes.start !== effect.nodes.end) { + // eslint-disable-next-line no-console + console.log(effect.nodes.end); + } + } + let child = effect.first; while (child !== null) { log_effect_tree(child, depth + 1); @@ -103,7 +139,13 @@ function log_dep(dep) { const derived = /** @type {Derived} */ (dep); // eslint-disable-next-line no-console - console.groupCollapsed('%cderived', 'font-weight: normal', derived.v); + console.groupCollapsed( + `%c$derived %c${dep.label ?? ''}`, + 'font-weight: bold; color: CornflowerBlue', + 'font-weight: normal', + untrack(() => snapshot(derived.v)) + ); + if (derived.deps) { for (const d of derived.deps) { log_dep(d); @@ -114,6 +156,345 @@ function log_dep(dep) { console.groupEnd(); } else { // eslint-disable-next-line no-console - console.log('state', dep.v); + console.log( + `%c$state %c${dep.label ?? ''}`, + 'font-weight: bold; color: CornflowerBlue', + 'font-weight: normal', + untrack(() => snapshot(dep.v)) + ); + } +} + +/** + * Logs all reactions of a source or derived transitively + * @param {Derived | Value} signal + */ +export function log_reactions(signal) { + /** @type {Set} */ + const visited = new Set(); + + /** + * Returns an array of flag names that are set on the given flags bitmask + * @param {number} flags + * @returns {string[]} + */ + function get_derived_flag_names(flags) { + /** @type {string[]} */ + const names = []; + + if ((flags & CLEAN) !== 0) names.push('CLEAN'); + if ((flags & DIRTY) !== 0) names.push('DIRTY'); + if ((flags & MAYBE_DIRTY) !== 0) names.push('MAYBE_DIRTY'); + if ((flags & CONNECTED) !== 0) names.push('CONNECTED'); + if ((flags & WAS_MARKED) !== 0) names.push('WAS_MARKED'); + if ((flags & INERT) !== 0) names.push('INERT'); + if ((flags & DESTROYED) !== 0) names.push('DESTROYED'); + + return names; } + + /** + * @param {Derived | Value} d + * @param {number} depth + */ + function log_derived(d, depth) { + const flags = d.f; + const flag_names = get_derived_flag_names(flags); + const flags_str = flag_names.length > 0 ? `(${flag_names.join(', ')})` : '(no flags)'; + + // eslint-disable-next-line no-console + console.group( + `%c${flags & DERIVED ? '$derived' : '$state'} %c${d.label ?? ''} %c${flags_str}`, + 'font-weight: bold; color: CornflowerBlue', + 'font-weight: normal; color: inherit', + 'font-weight: normal; color: gray' + ); + + // eslint-disable-next-line no-console + console.log(untrack(() => snapshot(d.v))); + + if ('fn' in d) { + // eslint-disable-next-line no-console + console.log('%cfn:', 'font-weight: bold', d.fn); + } + + if (d.reactions !== null && d.reactions.length > 0) { + // eslint-disable-next-line no-console + console.group('%creactions', 'font-weight: bold'); + + for (const reaction of d.reactions) { + if ((reaction.f & DERIVED) !== 0) { + const derived_reaction = /** @type {Derived} */ (reaction); + + if (visited.has(derived_reaction)) { + // eslint-disable-next-line no-console + console.log( + `%c$derived %c${derived_reaction.label ?? ''} %c(already seen)`, + 'font-weight: bold; color: CornflowerBlue', + 'font-weight: normal; color: inherit', + 'font-weight: bold; color: orange' + ); + } else { + visited.add(derived_reaction); + log_derived(derived_reaction, depth + 1); + } + } else { + // It's an effect + const label = effect_label(/** @type {Effect} */ (reaction), true); + const status = (flags & MAYBE_DIRTY) !== 0 ? 'maybe dirty' : 'dirty'; + + // Collect parent statuses + /** @type {string[]} */ + const parent_statuses = []; + let show = false; + let current = /** @type {Effect} */ (reaction).parent; + while (current !== null) { + const parent_flags = current.f; + if ((parent_flags & (ROOT_EFFECT | BRANCH_EFFECT)) !== 0) { + const parent_status = (parent_flags & CLEAN) !== 0 ? 'clean' : 'not clean'; + if (parent_status === 'clean' && parent_statuses.includes('not clean')) show = true; + parent_statuses.push(parent_status); + } + if (!current.parent) break; + current = current.parent; + } + + // Check if reaction is reachable from root + const seen_effects = new Set(); + let reachable = false; + /** + * @param {Effect | null} effect + */ + function check_reachable(effect) { + if (effect === null || reachable) return; + if (effect === reaction) { + reachable = true; + return; + } + if (effect.f & DESTROYED) return; + if (seen_effects.has(effect)) { + throw new Error(''); + } + seen_effects.add(effect); + let child = effect.first; + while (child !== null) { + check_reachable(child); + child = child.next; + } + } + try { + if (current) check_reachable(current); + } catch (e) { + // eslint-disable-next-line no-console + console.log( + `%c⚠️ Circular reference detected in effect tree`, + 'font-weight: bold; color: red', + seen_effects + ); + } + + if (!reachable) { + // eslint-disable-next-line no-console + console.log( + `%c⚠️ Effect is NOT reachable from its parent chain`, + 'font-weight: bold; color: red' + ); + } + + const parent_status_str = show ? ` (${parent_statuses.join(', ')})` : ''; + + // eslint-disable-next-line no-console + console.log( + `%c${label} (${status})${parent_status_str}`, + `font-weight: bold; color: ${parent_status_str ? 'red' : 'green'}`, + reaction + ); + } + } + + // eslint-disable-next-line no-console + console.groupEnd(); + } else { + // eslint-disable-next-line no-console + console.log('%cno reactions', 'font-style: italic; color: gray'); + } + + // eslint-disable-next-line no-console + console.groupEnd(); + } + + // eslint-disable-next-line no-console + console.group(`%cDerived Reactions Graph`, 'font-weight: bold; color: purple'); + + visited.add(signal); + log_derived(signal, 0); + + // eslint-disable-next-line no-console + console.groupEnd(); +} + +/** + * Traverses an effect tree and logs branches where a non-clean branch exists below a clean branch + * @param {Effect} effect + */ +export function log_inconsistent_branches(effect) { + const root_effect = root(effect); + + /** + * @typedef {{ + * effect: Effect, + * status: 'clean' | 'maybe dirty' | 'dirty', + * parent_clean: boolean, + * children: BranchInfo[] + * }} BranchInfo + */ + + /** + * Collects branch effects from the tree + * @param {Effect} eff + * @param {boolean} parent_clean - whether any ancestor branch is clean + * @returns {BranchInfo[]} + */ + function collect_branches(eff, parent_clean) { + /** @type {BranchInfo[]} */ + const branches = []; + const flags = eff.f; + const is_branch = (flags & BRANCH_EFFECT) !== 0; + + if (is_branch) { + const status = + (flags & CLEAN) !== 0 ? 'clean' : (flags & MAYBE_DIRTY) !== 0 ? 'maybe dirty' : 'dirty'; + + /** @type {BranchInfo[]} */ + const child_branches = []; + + let child = eff.first; + while (child !== null) { + child_branches.push(...collect_branches(child, status === 'clean')); + child = child.next; + } + + branches.push({ + effect: eff, + status, + parent_clean, + children: child_branches + }); + } else { + // Not a branch, continue traversing + let child = eff.first; + while (child !== null) { + branches.push(...collect_branches(child, parent_clean)); + child = child.next; + } + } + + return branches; + } + + /** + * Checks if a branch tree contains any inconsistencies (non-clean below clean) + * @param {BranchInfo} branch + * @param {boolean} ancestor_clean + * @returns {boolean} + */ + function has_inconsistency(branch, ancestor_clean) { + const is_inconsistent = ancestor_clean && branch.status !== 'clean'; + if (is_inconsistent) return true; + + const new_ancestor_clean = ancestor_clean || branch.status === 'clean'; + for (const child of branch.children) { + if (has_inconsistency(child, new_ancestor_clean)) return true; + } + return false; + } + + /** + * Logs a branch and its children, but only if there are inconsistencies + * @param {BranchInfo} branch + * @param {boolean} ancestor_clean + * @param {number} depth + */ + function log_branch(branch, ancestor_clean, depth) { + const is_inconsistent = ancestor_clean && branch.status !== 'clean'; + const new_ancestor_clean = ancestor_clean || branch.status === 'clean'; + + // Only log if this branch or any descendant has an inconsistency + if (!has_inconsistency(branch, ancestor_clean) && !is_inconsistent) { + return; + } + + const style = is_inconsistent + ? 'font-weight: bold; color: red' + : branch.status === 'clean' + ? 'font-weight: normal; color: green' + : 'font-weight: bold; color: orange'; + + const warning = is_inconsistent ? ' ⚠️ INCONSISTENT' : ''; + + // eslint-disable-next-line no-console + console.group(`%cbranch (${branch.status})${warning}`, style); + + // eslint-disable-next-line no-console + console.log('%ceffect:', 'font-weight: bold', branch.effect); + + if (branch.effect.fn) { + // eslint-disable-next-line no-console + console.log('%cfn:', 'font-weight: bold', branch.effect.fn); + } + + if (branch.effect.deps !== null) { + // eslint-disable-next-line no-console + console.groupCollapsed('%cdeps', 'font-weight: normal'); + for (const dep of branch.effect.deps) { + log_dep(dep); + } + // eslint-disable-next-line no-console + console.groupEnd(); + } + + if (is_inconsistent) { + log_effect_tree(branch.effect); + } else if (branch.children.length > 0) { + // eslint-disable-next-line no-console + console.group('%cchild branches', 'font-weight: bold'); + for (const child of branch.children) { + log_branch(child, new_ancestor_clean, depth + 1); + } + // eslint-disable-next-line no-console + console.groupEnd(); + } + + // eslint-disable-next-line no-console + console.groupEnd(); + } + + const branches = collect_branches(root_effect, false); + + // Check if there are any inconsistencies at all + let has_any_inconsistency = false; + for (const branch of branches) { + if (has_inconsistency(branch, false)) { + has_any_inconsistency = true; + break; + } + } + + if (!has_any_inconsistency) { + // eslint-disable-next-line no-console + console.log('%cNo inconsistent branches found', 'font-weight: bold; color: green'); + return; + } + + // eslint-disable-next-line no-console + console.group(`%cInconsistent Branches (non-clean below clean)`, 'font-weight: bold; color: red'); + + for (const branch of branches) { + log_branch(branch, false, 0); + } + + // eslint-disable-next-line no-console + console.groupEnd(); + + return true; } diff --git a/packages/svelte/src/internal/client/dev/hmr.js b/packages/svelte/src/internal/client/dev/hmr.js index 709a1b2722..9fa4e6ccbd 100644 --- a/packages/svelte/src/internal/client/dev/hmr.js +++ b/packages/svelte/src/internal/client/dev/hmr.js @@ -1,23 +1,25 @@ -/** @import { Source, Effect, TemplateNode } from '#client' */ +/** @import { Effect, TemplateNode } from '#client' */ import { FILENAME, HMR } from '../../../constants.js'; import { EFFECT_TRANSPARENT } from '#client/constants'; import { hydrate_node, hydrating } from '../dom/hydration.js'; import { block, branch, destroy_effect } from '../reactivity/effects.js'; -import { source } from '../reactivity/sources.js'; +import { set, source } from '../reactivity/sources.js'; import { set_should_intro } from '../render.js'; import { get } from '../runtime.js'; /** * @template {(anchor: Comment, props: any) => any} Component - * @param {Component} original - * @param {() => Source} get_source + * @param {Component} fn */ -export function hmr(original, get_source) { +export function hmr(fn) { + const current = source(fn); + /** * @param {TemplateNode} anchor * @param {any} props */ function wrapper(anchor, props) { + let component = {}; let instance = {}; /** @type {Effect} */ @@ -26,8 +28,9 @@ export function hmr(original, get_source) { let ran = false; block(() => { - const source = get_source(); - const component = get(source); + if (component === (component = get(current))) { + return; + } if (effect) { // @ts-ignore @@ -62,16 +65,24 @@ export function hmr(original, get_source) { } // @ts-expect-error - wrapper[FILENAME] = original[FILENAME]; + wrapper[FILENAME] = fn[FILENAME]; // @ts-ignore wrapper[HMR] = { - // When we accept an update, we set the original source to the new component - original, - // The `get_source` parameter reads `wrapper[HMR].source`, but in the `accept` - // function we always replace it with `previous[HMR].source`, which in practice - // means we only ever update the original - source: source(original) + fn, + current, + update: (/** @type {any} */ incoming) => { + // This logic ensures that the first version of the component is the one + // whose update function and therefore block effect is preserved across updates. + // If we don't do this dance and instead just use `incoming` as the new component + // and then update, we'll create an ever-growing stack of block effects. + + // Trigger the original block effect + set(wrapper[HMR].current, incoming[HMR].fn); + + // Replace the incoming source with the original one + incoming[HMR].current = wrapper[HMR].current; + } }; return wrapper; diff --git a/packages/svelte/src/internal/client/dev/inspect.js b/packages/svelte/src/internal/client/dev/inspect.js index db7ab0d976..75b29ce9b1 100644 --- a/packages/svelte/src/internal/client/dev/inspect.js +++ b/packages/svelte/src/internal/client/dev/inspect.js @@ -1,8 +1,8 @@ import { UNINITIALIZED } from '../../../constants.js'; import { snapshot } from '../../shared/clone.js'; -import { inspect_effect, render_effect, validate_effect } from '../reactivity/effects.js'; +import { eager_effect, render_effect, validate_effect } from '../reactivity/effects.js'; import { untrack } from '../runtime.js'; -import { get_stack } from './tracing.js'; +import { get_error } from '../../shared/dev.js'; /** * @param {() => any[]} get_value @@ -19,7 +19,7 @@ export function inspect(get_value, inspector, show_stack = false) { // stack traces. As a consequence, reading the value might result // in an error (an `$inspect(object.property)` will run before the // `{#if object}...{/if}` that contains it) - inspect_effect(() => { + eager_effect(() => { try { var value = get_value(); } catch (e) { @@ -33,8 +33,15 @@ export function inspect(get_value, inspector, show_stack = false) { inspector(...snap); if (!initial) { - // eslint-disable-next-line no-console - console.log(get_stack('UpdatedAt')); + const stack = get_error('$inspect(...)'); + if (stack) { + // eslint-disable-next-line no-console + console.groupCollapsed('stack trace'); + // eslint-disable-next-line no-console + console.log(stack); + // eslint-disable-next-line no-console + console.groupEnd(); + } } } else { inspector(initial ? 'init' : 'update', ...snap); diff --git a/packages/svelte/src/internal/client/dev/tracing.js b/packages/svelte/src/internal/client/dev/tracing.js index 95baefc64a..c6edfde933 100644 --- a/packages/svelte/src/internal/client/dev/tracing.js +++ b/packages/svelte/src/internal/client/dev/tracing.js @@ -1,7 +1,6 @@ /** @import { Derived, Reaction, Value } from '#client' */ import { UNINITIALIZED } from '../../../constants.js'; import { snapshot } from '../../shared/clone.js'; -import { define_property } from '../../shared/utils.js'; import { DERIVED, ASYNC, PROXY_PATH_SYMBOL, STATE_SYMBOL } from '#client/constants'; import { effect_tracking } from '../reactivity/effects.js'; import { active_reaction, untrack } from '../runtime.js'; @@ -57,8 +56,10 @@ function log_entry(signal, entry) { if (dirty && signal.updated) { for (const updated of signal.updated.values()) { - // eslint-disable-next-line no-console - console.log(updated.error); + if (updated.error) { + // eslint-disable-next-line no-console + console.log(updated.error); + } } } @@ -129,59 +130,6 @@ export function trace(label, fn) { } } -/** - * @param {string} label - * @returns {Error & { stack: string } | null} - */ -export function get_stack(label) { - // @ts-ignore stackTraceLimit doesn't exist everywhere - const limit = Error.stackTraceLimit; - - // @ts-ignore - Error.stackTraceLimit = Infinity; - let error = Error(); - - // @ts-ignore - Error.stackTraceLimit = limit; - - const stack = error.stack; - - if (!stack) return null; - - const lines = stack.split('\n'); - const new_lines = ['\n']; - - for (let i = 0; i < lines.length; i++) { - const line = lines[i]; - - if (line === 'Error') { - continue; - } - if (line.includes('validate_each_keys')) { - return null; - } - if (line.includes('svelte/src/internal') || line.includes('svelte\\src\\internal')) { - continue; - } - new_lines.push(line); - } - - if (new_lines.length === 1) { - return null; - } - - define_property(error, 'stack', { - value: new_lines.join('\n') - }); - - define_property(error, 'name', { - // 'Error' suffix is required for stack traces to be rendered properly - value: `${label}Error` - }); - - return /** @type {Error & { stack: string }} */ (error); -} - /** * @param {Value} source * @param {string} label diff --git a/packages/svelte/src/internal/client/dom/blocks/async.js b/packages/svelte/src/internal/client/dom/blocks/async.js index 5ee9d25bce..0e3ab33dda 100644 --- a/packages/svelte/src/internal/client/dom/blocks/async.js +++ b/packages/svelte/src/internal/client/dom/blocks/async.js @@ -1,4 +1,4 @@ -/** @import { TemplateNode, Value } from '#client' */ +/** @import { Blocker, TemplateNode, Value } from '#client' */ import { flatten } from '../../reactivity/async.js'; import { Batch, current_batch } from '../../reactivity/batch.js'; import { get } from '../../runtime.js'; @@ -14,28 +14,36 @@ import { get_boundary } from './boundary.js'; /** * @param {TemplateNode} node + * @param {Blocker[]} blockers * @param {Array<() => Promise>} expressions * @param {(anchor: TemplateNode, ...deriveds: Value[]) => void} fn */ -export function async(node, expressions, fn) { +export function async(node, blockers = [], expressions = [], fn) { + var was_hydrating = hydrating; + + if (was_hydrating) { + hydrate_next(); + } + + if (expressions.length === 0 && blockers.every((b) => b.settled)) { + fn(node); + return; + } + var boundary = get_boundary(); var batch = /** @type {Batch} */ (current_batch); - var blocking = !boundary.is_pending(); + var blocking = boundary.is_rendered(); boundary.update_pending_count(1); batch.increment(blocking); - var was_hydrating = hydrating; - if (was_hydrating) { - hydrate_next(); - var previous_hydrate_node = hydrate_node; var end = skip_nodes(false); set_hydrate_node(end); } - flatten([], expressions, (values) => { + flatten(blockers, [], expressions, (values) => { if (was_hydrating) { set_hydrating(true); set_hydrate_node(previous_hydrate_node); @@ -47,12 +55,12 @@ export function async(node, expressions, fn) { fn(node, ...values); } finally { + if (was_hydrating) { + set_hydrating(false); + } + boundary.update_pending_count(-1); batch.decrement(blocking); } - - if (was_hydrating) { - set_hydrating(false); - } }); } diff --git a/packages/svelte/src/internal/client/dom/blocks/await.js b/packages/svelte/src/internal/client/dom/blocks/await.js index bac01e4c33..87d64df23e 100644 --- a/packages/svelte/src/internal/client/dom/blocks/await.js +++ b/packages/svelte/src/internal/client/dom/blocks/await.js @@ -12,7 +12,7 @@ import { import { queue_micro_task } from '../task.js'; import { HYDRATION_START_ELSE, UNINITIALIZED } from '../../../../constants.js'; import { is_runes } from '../../context.js'; -import { flushSync, is_flushing_sync } from '../../reactivity/batch.js'; +import { Batch, flushSync, is_flushing_sync } from '../../reactivity/batch.js'; import { BranchManager } from './branches.js'; import { capture, unset_context } from '../../reactivity/async.js'; @@ -69,7 +69,11 @@ export function await_block(node, get_input, pending_fn, then_fn, catch_fn) { if (destroyed) return; resolved = true; - restore(); + // We don't want to restore the previous batch here; {#await} blocks don't follow the async logic + // we have elsewhere, instead pending/resolve/fail states are each their own batch so to speak. + restore(false); + // Make sure we have a batch, since the branch manager expects one to exist + Batch.ensure(); if (hydrating) { // `restore()` could set `hydrating` to `true`, which we very much diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index 3da9204571..ef5f0e116d 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -1,9 +1,12 @@ /** @import { Effect, Source, TemplateNode, } from '#client' */ import { + BLOCK_EFFECT, BOUNDARY_EFFECT, COMMENT_NODE, + DIRTY, EFFECT_PRESERVED, - EFFECT_TRANSPARENT + EFFECT_TRANSPARENT, + MAYBE_DIRTY } from '#client/constants'; import { HYDRATION_START_ELSE } from '../../../../constants.js'; import { component_context, set_component_context } from '../../context.js'; @@ -30,15 +33,17 @@ import { skip_nodes, set_hydrate_node } from '../hydration.js'; -import { get_next_sibling } from '../operations.js'; import { queue_micro_task } from '../task.js'; import * as e from '../../errors.js'; import * as w from '../../warnings.js'; import { DEV } from 'esm-env'; -import { Batch, effect_pending_updates } from '../../reactivity/batch.js'; +import { Batch, schedule_effect } from '../../reactivity/batch.js'; import { internal_set, source } from '../../reactivity/sources.js'; import { tag } from '../../dev/tracing.js'; import { createSubscriber } from '../../../../reactivity/create-subscriber.js'; +import { create_text } from '../operations.js'; +import { defer_effect } from '../../reactivity/utils.js'; +import { set_signal_status } from '../../reactivity/status.js'; /** * @typedef {{ @@ -64,7 +69,7 @@ export class Boundary { /** @type {Boundary | null} */ parent; - #pending = false; + is_pending = false; /** @type {TemplateNode} */ #anchor; @@ -93,11 +98,21 @@ export class Boundary { /** @type {DocumentFragment | null} */ #offscreen_fragment = null; + /** @type {TemplateNode | null} */ + #pending_anchor = null; + #local_pending_count = 0; #pending_count = 0; + #pending_count_update_queued = false; #is_creating_fallback = false; + /** @type {Set} */ + #dirty_effects = new Set(); + + /** @type {Set} */ + #maybe_dirty_effects = new Set(); + /** * A source containing the number of pending async deriveds/expressions. * Only created if `$effect.pending()` is used inside the boundary, @@ -107,12 +122,6 @@ export class Boundary { */ #effect_pending = null; - #effect_pending_update = () => { - if (this.#effect_pending) { - internal_set(this.#effect_pending, this.#local_pending_count); - } - }; - #effect_pending_subscriber = createSubscriber(() => { this.#effect_pending = source(this.#local_pending_count); @@ -137,7 +146,7 @@ export class Boundary { this.parent = /** @type {Effect} */ (active_effect).b; - this.#pending = !!this.#props.pending; + this.is_pending = !!this.#props.pending; this.#effect = block(() => { /** @type {Effect} */ (active_effect).b = this; @@ -154,10 +163,16 @@ export class Boundary { this.#hydrate_pending_content(); } else { this.#hydrate_resolved_content(); + + if (this.#pending_count === 0) { + this.is_pending = false; + } } } else { + var anchor = this.#get_anchor(); + try { - this.#main_effect = branch(() => children(this.#anchor)); + this.#main_effect = branch(() => children(anchor)); } catch (error) { this.error(error); } @@ -165,9 +180,13 @@ export class Boundary { if (this.#pending_count > 0) { this.#show_pending_snippet(); } else { - this.#pending = false; + this.is_pending = false; } } + + return () => { + this.#pending_anchor?.remove(); + }; }, flags); if (hydrating) { @@ -181,23 +200,20 @@ export class Boundary { } catch (error) { this.error(error); } - - // Since server rendered resolved content, we never show pending state - // Even if client-side async operations are still running, the content is already displayed - this.#pending = false; } #hydrate_pending_content() { const pending = this.#props.pending; - if (!pending) { - return; - } + if (!pending) return; + this.#pending_effect = branch(() => pending(this.#anchor)); - Batch.enqueue(() => { + queue_micro_task(() => { + var anchor = this.#get_anchor(); + this.#main_effect = this.#run(() => { Batch.ensure(); - return branch(() => this.#children(this.#anchor)); + return branch(() => this.#children(anchor)); }); if (this.#pending_count > 0) { @@ -207,17 +223,38 @@ export class Boundary { this.#pending_effect = null; }); - this.#pending = false; + this.is_pending = false; } }); } + #get_anchor() { + var anchor = this.#anchor; + + if (this.is_pending) { + this.#pending_anchor = create_text(); + this.#anchor.before(this.#pending_anchor); + + anchor = this.#pending_anchor; + } + + return anchor; + } + + /** + * Defer an effect inside a pending boundary until the boundary resolves + * @param {Effect} effect + */ + defer_effect(effect) { + defer_effect(effect, this.#dirty_effects, this.#maybe_dirty_effects); + } + /** - * Returns `true` if the effect exists inside a boundary whose pending snippet is shown + * Returns `false` if the effect exists inside a boundary whose pending snippet is shown * @returns {boolean} */ - is_pending() { - return this.#pending || (!!this.parent && this.parent.is_pending()); + is_rendered() { + return !this.is_pending && (!this.parent || this.parent.is_rendered()); } has_pending_snippet() { @@ -253,6 +290,7 @@ export class Boundary { if (this.#main_effect !== null) { this.#offscreen_fragment = document.createDocumentFragment(); + this.#offscreen_fragment.append(/** @type {TemplateNode} */ (this.#pending_anchor)); move_effect(this.#main_effect, this.#offscreen_fragment); } @@ -279,7 +317,24 @@ export class Boundary { this.#pending_count += d; if (this.#pending_count === 0) { - this.#pending = false; + this.is_pending = false; + + // any effects that were encountered and deferred during traversal + // should be rescheduled — after the next traversal (which will happen + // immediately, due to the same update that brought us here) + // the effects will be flushed + for (const e of this.#dirty_effects) { + set_signal_status(e, DIRTY); + schedule_effect(e); + } + + for (const e of this.#maybe_dirty_effects) { + set_signal_status(e, MAYBE_DIRTY); + schedule_effect(e); + } + + this.#dirty_effects.clear(); + this.#maybe_dirty_effects.clear(); if (this.#pending_effect) { pause_effect(this.#pending_effect, () => { @@ -304,7 +359,16 @@ export class Boundary { this.#update_pending_count(d); this.#local_pending_count += d; - effect_pending_updates.add(this.#effect_pending_update); + + if (!this.#effect_pending || this.#pending_count_update_queued) return; + this.#pending_count_update_queued = true; + + queue_micro_task(() => { + this.#pending_count_update_queued = false; + if (this.#effect_pending) { + internal_set(this.#effect_pending, this.#local_pending_count); + } + }); } get_effect_pending() { @@ -372,7 +436,7 @@ export class Boundary { // we intentionally do not try to find the nearest pending boundary. If this boundary has one, we'll render it on reset // but it would be really weird to show the parent's boundary on a child reset. - this.#pending = this.has_pending_snippet(); + this.is_pending = this.has_pending_snippet(); this.#main_effect = this.#run(() => { this.#is_creating_fallback = false; @@ -382,26 +446,22 @@ export class Boundary { if (this.#pending_count > 0) { this.#show_pending_snippet(); } else { - this.#pending = false; + this.is_pending = false; } }; - var previous_reaction = active_reaction; - - try { - set_active_reaction(null); - calling_on_error = true; - onerror?.(error, reset); - calling_on_error = false; - } catch (error) { - invoke_error_boundary(error, this.#effect && this.#effect.parent); - } finally { - set_active_reaction(previous_reaction); - } + queue_micro_task(() => { + try { + calling_on_error = true; + onerror?.(error, reset); + calling_on_error = false; + } catch (error) { + invoke_error_boundary(error, this.#effect && this.#effect.parent); + } - if (failed) { - queue_micro_task(() => { + if (failed) { this.#failed_effect = this.#run(() => { + Batch.ensure(); this.#is_creating_fallback = true; try { @@ -419,8 +479,8 @@ export class Boundary { this.#is_creating_fallback = false; } }); - }); - } + } + }); } } diff --git a/packages/svelte/src/internal/client/dom/blocks/branches.js b/packages/svelte/src/internal/client/dom/blocks/branches.js index 827f9f44fa..527f0b0a8f 100644 --- a/packages/svelte/src/internal/client/dom/blocks/branches.js +++ b/packages/svelte/src/internal/client/dom/blocks/branches.js @@ -1,5 +1,4 @@ /** @import { Effect, TemplateNode } from '#client' */ -import { is_runes } from '../../context.js'; import { Batch, current_batch } from '../../reactivity/batch.js'; import { branch, @@ -8,7 +7,6 @@ import { pause_effect, resume_effect } from '../../reactivity/effects.js'; -import { set_should_intro, should_intro } from '../../render.js'; import { hydrate_node, hydrating } from '../hydration.js'; import { create_text, should_defer_append } from '../operations.js'; @@ -26,12 +24,35 @@ export class BranchManager { /** @type {Map} */ #batches = new Map(); - /** @type {Map} */ + /** + * Map of keys to effects that are currently rendered in the DOM. + * These effects are visible and actively part of the document tree. + * Example: + * ``` + * {#if condition} + * foo + * {:else} + * bar + * {/if} + * ``` + * Can result in the entries `true->Effect` and `false->Effect` + * @type {Map} + */ #onscreen = new Map(); - /** @type {Map} */ + /** + * Similar to #onscreen with respect to the keys, but contains branches that are not yet + * in the DOM, because their insertion is deferred. + * @type {Map} + */ #offscreen = new Map(); + /** + * Keys of effects that are currently outroing + * @type {Set} + */ + #outroing = new Set(); + /** * Whether to pause (i.e. outro) on change, or destroy immediately. * This is necessary for `` @@ -60,6 +81,7 @@ export class BranchManager { if (onscreen) { // effect is already in the DOM — abort any current outro resume_effect(onscreen); + this.#outroing.delete(key); } else { // effect is currently offscreen. put it in the DOM var offscreen = this.#offscreen.get(key); @@ -98,7 +120,8 @@ export class BranchManager { // outro/destroy all onscreen effects... for (const [k, effect] of this.#onscreen) { // ...except the one that was just committed - if (k === key) continue; + // or those that are already outroing (else the transition is aborted and the effect destroyed right away) + if (k === key || this.#outroing.has(k)) continue; const on_destroy = () => { const keys = Array.from(this.#batches.values()); @@ -115,10 +138,12 @@ export class BranchManager { destroy_effect(effect); } + this.#outroing.delete(k); this.#onscreen.delete(k); }; if (this.#transition || !onscreen) { + this.#outroing.add(k); pause_effect(effect, on_destroy, false); } else { on_destroy(); @@ -126,6 +151,22 @@ export class BranchManager { } }; + /** + * @param {Batch} batch + */ + #discard = (batch) => { + this.#batches.delete(batch); + + const keys = Array.from(this.#batches.values()); + + for (const [k, branch] of this.#offscreen) { + if (!keys.includes(k)) { + destroy_effect(branch.effect); + this.#offscreen.delete(k); + } + } + }; + /** * * @param {any} key @@ -173,7 +214,8 @@ export class BranchManager { } } - batch.add_callback(this.#commit); + batch.oncommit(this.#commit); + batch.ondiscard(this.#discard); } else { if (hydrating) { this.anchor = hydrate_node; diff --git a/packages/svelte/src/internal/client/dom/blocks/css-props.js b/packages/svelte/src/internal/client/dom/blocks/css-props.js index ef36198753..084a5561be 100644 --- a/packages/svelte/src/internal/client/dom/blocks/css-props.js +++ b/packages/svelte/src/internal/client/dom/blocks/css-props.js @@ -1,5 +1,4 @@ -/** @import { TemplateNode } from '#client' */ -import { render_effect, teardown } from '../../reactivity/effects.js'; +import { render_effect } from '../../reactivity/effects.js'; import { hydrating, set_hydrate_node } from '../hydration.js'; import { get_first_child } from '../operations.js'; @@ -10,7 +9,7 @@ import { get_first_child } from '../operations.js'; */ export function css_props(element, get_styles) { if (hydrating) { - set_hydrate_node(/** @type {TemplateNode} */ (get_first_child(element))); + set_hydrate_node(get_first_child(element)); } render_effect(() => { diff --git a/packages/svelte/src/internal/client/dom/blocks/each.js b/packages/svelte/src/internal/client/dom/blocks/each.js index a6369a7211..232656ec11 100644 --- a/packages/svelte/src/internal/client/dom/blocks/each.js +++ b/packages/svelte/src/internal/client/dom/blocks/each.js @@ -1,4 +1,4 @@ -/** @import { EachItem, EachState, Effect, MaybeSource, Source, TemplateNode, TransitionManager, Value } from '#client' */ +/** @import { EachItem, EachOutroGroup, EachState, Effect, EffectNodes, MaybeSource, Source, TemplateNode, TransitionManager, Value } from '#client' */ /** @import { Batch } from '../../reactivity/batch.js'; */ import { EACH_INDEX_REACTIVE, @@ -29,31 +29,21 @@ import { block, branch, destroy_effect, - run_out_transitions, - pause_children, pause_effect, resume_effect } from '../../reactivity/effects.js'; import { source, mutable_source, internal_set } from '../../reactivity/sources.js'; import { array_from, is_array } from '../../../shared/utils.js'; -import { COMMENT_NODE, INERT } from '#client/constants'; +import { BRANCH_EFFECT, COMMENT_NODE, EFFECT_OFFSCREEN, INERT } from '#client/constants'; import { queue_micro_task } from '../task.js'; -import { active_effect, get } from '../../runtime.js'; +import { get } from '../../runtime.js'; import { DEV } from 'esm-env'; import { derived_safe_equal } from '../../reactivity/deriveds.js'; import { current_batch } from '../../reactivity/batch.js'; -/** - * The row of a keyed each block that is currently updating. We track this - * so that `animate:` directives have something to attach themselves to - * @type {EachItem | null} - */ -export let current_each_item = null; - -/** @param {EachItem | null} item */ -export function set_current_each_item(item) { - current_each_item = item; -} +// When making substantive changes to this file, validate them with the each block stress test: +// https://svelte.dev/playground/1972b2cf46564476ad8c8c6405b23b7b +// This test also exists in this repo, as `packages/svelte/tests/manual/each-stress-test` /** * @param {any} _ @@ -67,45 +57,88 @@ export function index(_, i) { * Pause multiple effects simultaneously, and coordinate their * subsequent destruction. Used in each blocks * @param {EachState} state - * @param {EachItem[]} items + * @param {Effect[]} to_destroy * @param {null | Node} controlled_anchor */ -function pause_effects(state, items, controlled_anchor) { - var items_map = state.items; - +function pause_effects(state, to_destroy, controlled_anchor) { /** @type {TransitionManager[]} */ var transitions = []; - var length = items.length; + var length = to_destroy.length; + + /** @type {EachOutroGroup} */ + var group; + var remaining = to_destroy.length; for (var i = 0; i < length; i++) { - pause_children(items[i].e, transitions, true); - } + let effect = to_destroy[i]; - var is_controlled = length > 0 && transitions.length === 0 && controlled_anchor !== null; - // If we have a controlled anchor, it means that the each block is inside a single - // DOM element, so we can apply a fast-path for clearing the contents of the element. - if (is_controlled) { - var parent_node = /** @type {Element} */ ( - /** @type {Element} */ (controlled_anchor).parentNode + pause_effect( + effect, + () => { + if (group) { + group.pending.delete(effect); + group.done.add(effect); + + if (group.pending.size === 0) { + var groups = /** @type {Set} */ (state.outrogroups); + + destroy_effects(array_from(group.done)); + groups.delete(group); + + if (groups.size === 0) { + state.outrogroups = null; + } + } + } else { + remaining -= 1; + } + }, + false ); - clear_text_content(parent_node); - parent_node.append(/** @type {Element} */ (controlled_anchor)); - items_map.clear(); - link(state, items[0].prev, items[length - 1].next); } - run_out_transitions(transitions, () => { - for (var i = 0; i < length; i++) { - var item = items[i]; - if (!is_controlled) { - items_map.delete(item.k); - link(state, item.prev, item.next); - } - destroy_effect(item.e, !is_controlled); + if (remaining === 0) { + // If we're in a controlled each block (i.e. the block is the only child of an + // element), and we are removing all items, _and_ there are no out transitions, + // we can use the fast path — emptying the element and replacing the anchor + var fast_path = transitions.length === 0 && controlled_anchor !== null; + + if (fast_path) { + var anchor = /** @type {Element} */ (controlled_anchor); + var parent_node = /** @type {Element} */ (anchor.parentNode); + + clear_text_content(parent_node); + parent_node.append(anchor); + + state.items.clear(); } - }); + + destroy_effects(to_destroy, !fast_path); + } else { + group = { + pending: new Set(to_destroy), + done: new Set() + }; + + (state.outrogroups ??= new Set()).add(group); + } +} + +/** + * @param {Effect[]} to_destroy + * @param {boolean} remove_dom + */ +function destroy_effects(to_destroy, remove_dom = true) { + // TODO only destroy effects if no pending batch needs them. otherwise, + // just re-add the `EFFECT_OFFSCREEN` flag + for (var i = 0; i < to_destroy.length; i++) { + destroy_effect(to_destroy[i], remove_dom); + } } +/** @type {TemplateNode} */ +var offscreen_anchor; + /** * @template V * @param {Element | Comment} node The next sibling node, or the parent node if this is a 'controlled' block @@ -119,8 +152,8 @@ function pause_effects(state, items, controlled_anchor) { export function each(node, flags, get_collection, get_key, render_fn, fallback_fn = null) { var anchor = node; - /** @type {EachState} */ - var state = { flags, items: new Map(), first: null }; + /** @type {Map} */ + var items = new Map(); var is_controlled = (flags & EACH_IS_CONTROLLED) !== 0; @@ -128,7 +161,7 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f var parent_node = /** @type {Element} */ (node); anchor = hydrating - ? set_hydrate_node(/** @type {Comment | Text} */ (get_first_child(parent_node))) + ? set_hydrate_node(get_first_child(parent_node)) : parent_node.appendChild(create_text()); } @@ -139,11 +172,6 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f /** @type {Effect | null} */ var fallback = null; - var was_empty = false; - - /** @type {Map} */ - var offscreen_items = new Map(); - // TODO: ideally we could use derived for runes mode but because of the ability // to use a store which can be mutated, we can't do that here as mutating a store // will still result in the collection array being the same from the store @@ -156,51 +184,35 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f /** @type {V[]} */ var array; - /** @type {Effect} */ - var each_effect; + var first_run = true; function commit() { - reconcile( - each_effect, - array, - state, - offscreen_items, - anchor, - render_fn, - flags, - get_key, - get_collection - ); + state.fallback = fallback; + reconcile(state, array, anchor, flags, get_key); - if (fallback_fn !== null) { + if (fallback !== null) { if (array.length === 0) { - if (fallback) { + if ((fallback.f & EFFECT_OFFSCREEN) === 0) { resume_effect(fallback); } else { - fallback = branch(() => fallback_fn(anchor)); + fallback.f ^= EFFECT_OFFSCREEN; + move(fallback, null, anchor); } - } else if (fallback !== null) { + } else { pause_effect(fallback, () => { + // TODO only null out if no pending batch needs it, + // otherwise re-add `fallback.fragment` and move the + // effect into it fallback = null; }); } } } - block(() => { - // store a reference to the effect so that we can update the start/end nodes in reconciliation - each_effect ??= /** @type {Effect} */ (active_effect); - + var effect = block(() => { array = /** @type {V[]} */ (get(each_array)); var length = array.length; - if (was_empty && length === 0) { - // ignore updates if the array is empty, - // and it already was empty on previous run - return; - } - was_empty = length === 0; - /** `true` if there was a hydration mismatch. Needs to be a `let` or else it isn't treeshaken out */ let mismatch = false; @@ -217,100 +229,84 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f } } - // this is separate to the previous block because `hydrating` might change - if (hydrating) { - /** @type {EachItem | null} */ - var prev = null; - - /** @type {EachItem} */ - var item; - - for (var i = 0; i < length; i++) { - if ( - hydrate_node.nodeType === COMMENT_NODE && - /** @type {Comment} */ (hydrate_node).data === HYDRATION_END - ) { - // The server rendered fewer items than expected, - // so break out and continue appending non-hydrated items - anchor = /** @type {Comment} */ (hydrate_node); - mismatch = true; - set_hydrating(false); - break; - } + var keys = new Set(); + var batch = /** @type {Batch} */ (current_batch); + var defer = should_defer_append(); + + for (var index = 0; index < length; index += 1) { + if ( + hydrating && + hydrate_node.nodeType === COMMENT_NODE && + /** @type {Comment} */ (hydrate_node).data === HYDRATION_END + ) { + // The server rendered fewer items than expected, + // so break out and continue appending non-hydrated items + anchor = /** @type {Comment} */ (hydrate_node); + mismatch = true; + set_hydrating(false); + } - var value = array[i]; - var key = get_key(value, i); + var value = array[index]; + var key = get_key(value, index); + + var item = first_run ? null : items.get(key); + + if (item) { + // update before reconciliation, to trigger any async updates + if (item.v) internal_set(item.v, value); + if (item.i) internal_set(item.i, index); + + if (defer) { + batch.skipped_effects.delete(item.e); + } + } else { item = create_item( - hydrate_node, - state, - prev, - null, + items, + first_run ? anchor : (offscreen_anchor ??= create_text()), value, key, - i, + index, render_fn, flags, get_collection ); - state.items.set(key, item); - prev = item; - } + if (!first_run) { + item.e.f |= EFFECT_OFFSCREEN; + } - // remove excess nodes - if (length > 0) { - set_hydrate_node(skip_nodes()); + items.set(key, item); } + + keys.add(key); } - if (hydrating) { - if (length === 0 && fallback_fn) { + if (length === 0 && fallback_fn && !fallback) { + if (first_run) { fallback = branch(() => fallback_fn(anchor)); + } else { + fallback = branch(() => fallback_fn((offscreen_anchor ??= create_text()))); + fallback.f |= EFFECT_OFFSCREEN; } - } else { - if (should_defer_append()) { - var keys = new Set(); - var batch = /** @type {Batch} */ (current_batch); - - for (i = 0; i < length; i += 1) { - value = array[i]; - key = get_key(value, i); - - var existing = state.items.get(key) ?? offscreen_items.get(key); - - if (existing) { - // update before reconciliation, to trigger any async updates - if ((flags & (EACH_ITEM_REACTIVE | EACH_INDEX_REACTIVE)) !== 0) { - update_item(existing, value, i, flags); - } - } else { - item = create_item( - null, - state, - null, - null, - value, - key, - i, - render_fn, - flags, - get_collection, - true - ); - - offscreen_items.set(key, item); - } + } - keys.add(key); - } + // remove excess nodes + if (hydrating && length > 0) { + set_hydrate_node(skip_nodes()); + } - for (const [key, item] of state.items) { + if (!first_run) { + if (defer) { + for (const [key, item] of items) { if (!keys.has(key)) { batch.skipped_effects.add(item.e); } } - batch.add_callback(commit); + batch.oncommit(commit); + batch.ondiscard(() => { + // TODO presumably we need to do something here? + }); } else { commit(); } @@ -330,57 +326,58 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f get(each_array); }); + /** @type {EachState} */ + var state = { effect, flags, items, outrogroups: null, fallback }; + + first_run = false; + if (hydrating) { anchor = hydrate_node; } } +/** + * Skip past any non-branch effects (which could be created with `createSubscriber`, for example) to find the next branch effect + * @param {Effect | null} effect + * @returns {Effect | null} + */ +function skip_to_branch(effect) { + while (effect !== null && (effect.f & BRANCH_EFFECT) === 0) { + effect = effect.next; + } + return effect; +} + /** * Add, remove, or reorder items output by an each block as its input changes * @template V - * @param {Effect} each_effect - * @param {Array} array * @param {EachState} state - * @param {Map} offscreen_items + * @param {Array} array * @param {Element | Comment | Text} anchor - * @param {(anchor: Node, item: MaybeSource, index: number | Source, collection: () => V[]) => void} render_fn * @param {number} flags * @param {(value: V, index: number) => any} get_key - * @param {() => V[]} get_collection * @returns {void} */ -function reconcile( - each_effect, - array, - state, - offscreen_items, - anchor, - render_fn, - flags, - get_key, - get_collection -) { +function reconcile(state, array, anchor, flags, get_key) { var is_animated = (flags & EACH_IS_ANIMATED) !== 0; - var should_update = (flags & (EACH_ITEM_REACTIVE | EACH_INDEX_REACTIVE)) !== 0; var length = array.length; var items = state.items; - var first = state.first; - var current = first; + var current = skip_to_branch(state.effect.first); - /** @type {undefined | Set} */ + /** @type {undefined | Set} */ var seen; - /** @type {EachItem | null} */ + /** @type {Effect | null} */ var prev = null; - /** @type {undefined | Set} */ + /** @type {undefined | Set} */ var to_animate; - /** @type {EachItem[]} */ + /** @type {Effect[]} */ var matched = []; - /** @type {EachItem[]} */ + /** @type {Effect[]} */ var stashed = []; /** @type {V} */ @@ -389,8 +386,8 @@ function reconcile( /** @type {any} */ var key; - /** @type {EachItem | undefined} */ - var item; + /** @type {Effect | undefined} */ + var effect; /** @type {number} */ var i; @@ -399,11 +396,13 @@ function reconcile( for (i = 0; i < length; i += 1) { value = array[i]; key = get_key(value, i); - item = items.get(key); + effect = /** @type {EachItem} */ (items.get(key)).e; - if (item !== undefined) { - item.a?.measure(); - (to_animate ??= new Set()).add(item); + // offscreen == coming in now, no animation in that case, + // else this would happen https://github.com/sveltejs/svelte/issues/17181 + if ((effect.f & EFFECT_OFFSCREEN) === 0) { + effect.nodes?.a?.measure(); + (to_animate ??= new Set()).add(effect); } } } @@ -412,62 +411,53 @@ function reconcile( value = array[i]; key = get_key(value, i); - item = items.get(key); + effect = /** @type {EachItem} */ (items.get(key)).e; - if (item === undefined) { - var pending = offscreen_items.get(key); + if (state.outrogroups !== null) { + for (const group of state.outrogroups) { + group.pending.delete(effect); + group.done.delete(effect); + } + } - if (pending !== undefined) { - offscreen_items.delete(key); - items.set(key, pending); + if ((effect.f & EFFECT_OFFSCREEN) !== 0) { + effect.f ^= EFFECT_OFFSCREEN; + if (effect === current) { + move(effect, null, anchor); + } else { var next = prev ? prev.next : current; - link(state, prev, pending); - link(state, pending, next); + if (effect === state.effect.last) { + state.effect.last = effect.prev; + } - move(pending, next, anchor); - prev = pending; - } else { - var child_anchor = current ? /** @type {TemplateNode} */ (current.e.nodes_start) : anchor; + if (effect.prev) effect.prev.next = effect.next; + if (effect.next) effect.next.prev = effect.prev; + link(state, prev, effect); + link(state, effect, next); - prev = create_item( - child_anchor, - state, - prev, - prev === null ? state.first : prev.next, - value, - key, - i, - render_fn, - flags, - get_collection - ); - } - - items.set(key, prev); + move(effect, next, anchor); + prev = effect; - matched = []; - stashed = []; + matched = []; + stashed = []; - current = prev.next; - continue; - } - - if (should_update) { - update_item(item, value, i, flags); + current = skip_to_branch(prev.next); + continue; + } } - if ((item.e.f & INERT) !== 0) { - resume_effect(item.e); + if ((effect.f & INERT) !== 0) { + resume_effect(effect); if (is_animated) { - item.a?.unfix(); - (to_animate ??= new Set()).delete(item); + effect.nodes?.a?.unfix(); + (to_animate ??= new Set()).delete(effect); } } - if (item !== current) { - if (seen !== undefined && seen.has(item)) { + if (effect !== current) { + if (seen !== undefined && seen.has(effect)) { if (matched.length < stashed.length) { // more efficient to move later items to the front var start = stashed[0]; @@ -498,14 +488,14 @@ function reconcile( stashed = []; } else { // more efficient to move earlier items to the back - seen.delete(item); - move(item, current, anchor); + seen.delete(effect); + move(effect, current, anchor); - link(state, item.prev, item.next); - link(state, item, prev === null ? state.first : prev.next); - link(state, prev, item); + link(state, effect.prev, effect.next); + link(state, effect, prev === null ? state.effect.first : prev.next); + link(state, prev, effect); - prev = item; + prev = effect; } continue; @@ -514,37 +504,57 @@ function reconcile( matched = []; stashed = []; - while (current !== null && current.k !== key) { - // If the each block isn't inert and an item has an effect that is already inert, - // skip over adding it to our seen Set as the item is already being handled - if ((current.e.f & INERT) === 0) { - (seen ??= new Set()).add(current); - } + while (current !== null && current !== effect) { + (seen ??= new Set()).add(current); stashed.push(current); - current = current.next; + current = skip_to_branch(current.next); } if (current === null) { continue; } + } + + if ((effect.f & EFFECT_OFFSCREEN) === 0) { + matched.push(effect); + } + + prev = effect; + current = skip_to_branch(effect.next); + } - item = current; + if (state.outrogroups !== null) { + for (const group of state.outrogroups) { + if (group.pending.size === 0) { + destroy_effects(array_from(group.done)); + state.outrogroups?.delete(group); + } } - matched.push(item); - prev = item; - current = item.next; + if (state.outrogroups.size === 0) { + state.outrogroups = null; + } } if (current !== null || seen !== undefined) { - var to_destroy = seen === undefined ? [] : array_from(seen); + /** @type {Effect[]} */ + var to_destroy = []; + + if (seen !== undefined) { + for (effect of seen) { + if ((effect.f & INERT) === 0) { + to_destroy.push(effect); + } + } + } while (current !== null) { // If the each block isn't inert, then inert effects are currently outroing and will be removed once the transition is finished - if ((current.e.f & INERT) === 0) { + if ((current.f & INERT) === 0 && current !== state.fallback) { to_destroy.push(current); } - current = current.next; + + current = skip_to_branch(current.next); } var destroy_length = to_destroy.length; @@ -554,11 +564,11 @@ function reconcile( if (is_animated) { for (i = 0; i < destroy_length; i += 1) { - to_destroy[i].a?.measure(); + to_destroy[i].nodes?.a?.measure(); } for (i = 0; i < destroy_length; i += 1) { - to_destroy[i].a?.fix(); + to_destroy[i].nodes?.a?.fix(); } } @@ -569,164 +579,100 @@ function reconcile( if (is_animated) { queue_micro_task(() => { if (to_animate === undefined) return; - for (item of to_animate) { - item.a?.apply(); + for (effect of to_animate) { + effect.nodes?.a?.apply(); } }); } - - each_effect.first = state.first && state.first.e; - each_effect.last = prev && prev.e; - - for (var unused of offscreen_items.values()) { - destroy_effect(unused.e); - } - - offscreen_items.clear(); -} - -/** - * @param {EachItem} item - * @param {any} value - * @param {number} index - * @param {number} type - * @returns {void} - */ -function update_item(item, value, index, type) { - if ((type & EACH_ITEM_REACTIVE) !== 0) { - internal_set(item.v, value); - } - - if ((type & EACH_INDEX_REACTIVE) !== 0) { - internal_set(/** @type {Value} */ (item.i), index); - } else { - item.i = index; - } } /** * @template V - * @param {Node | null} anchor - * @param {EachState} state - * @param {EachItem | null} prev - * @param {EachItem | null} next + * @param {Map} items + * @param {Node} anchor * @param {V} value * @param {unknown} key * @param {number} index * @param {(anchor: Node, item: V | Source, index: number | Value, collection: () => V[]) => void} render_fn * @param {number} flags * @param {() => V[]} get_collection - * @param {boolean} [deferred] * @returns {EachItem} */ -function create_item( - anchor, - state, - prev, - next, - value, - key, - index, - render_fn, - flags, - get_collection, - deferred -) { - var previous_each_item = current_each_item; - var reactive = (flags & EACH_ITEM_REACTIVE) !== 0; - var mutable = (flags & EACH_ITEM_IMMUTABLE) === 0; - - var v = reactive ? (mutable ? mutable_source(value, false, false) : source(value)) : value; - var i = (flags & EACH_INDEX_REACTIVE) === 0 ? index : source(index); - - if (DEV && reactive) { +function create_item(items, anchor, value, key, index, render_fn, flags, get_collection) { + var v = + (flags & EACH_ITEM_REACTIVE) !== 0 + ? (flags & EACH_ITEM_IMMUTABLE) === 0 + ? mutable_source(value, false, false) + : source(value) + : null; + + var i = (flags & EACH_INDEX_REACTIVE) !== 0 ? source(index) : null; + + if (DEV && v) { // For tracing purposes, we need to link the source signal we create with the // collection + index so that tracing works as intended - /** @type {Value} */ (v).trace = () => { - var collection_index = typeof i === 'number' ? index : i.v; + v.trace = () => { // eslint-disable-next-line @typescript-eslint/no-unused-expressions - get_collection()[collection_index]; + get_collection()[i?.v ?? index]; }; } - /** @type {EachItem} */ - var item = { - i, + return { v, - k: key, - a: null, - // @ts-expect-error - e: null, - prev, - next - }; - - current_each_item = item; - - try { - if (anchor === null) { - var fragment = document.createDocumentFragment(); - fragment.append((anchor = create_text())); - } - - item.e = branch(() => render_fn(/** @type {Node} */ (anchor), v, i, get_collection), hydrating); - - item.e.prev = prev && prev.e; - item.e.next = next && next.e; - - if (prev === null) { - if (!deferred) { - state.first = item; - } - } else { - prev.next = item; - prev.e.next = item.e; - } - - if (next !== null) { - next.prev = item; - next.e.prev = item.e; - } + i, + e: branch(() => { + render_fn(anchor, v ?? value, i ?? index, get_collection); - return item; - } finally { - current_each_item = previous_each_item; - } + return () => { + items.delete(key); + }; + }) + }; } /** - * @param {EachItem} item - * @param {EachItem | null} next + * @param {Effect} effect + * @param {Effect | null} next * @param {Text | Element | Comment} anchor */ -function move(item, next, anchor) { - var end = item.next ? /** @type {TemplateNode} */ (item.next.e.nodes_start) : anchor; +function move(effect, next, anchor) { + if (!effect.nodes) return; - var dest = next ? /** @type {TemplateNode} */ (next.e.nodes_start) : anchor; - var node = /** @type {TemplateNode} */ (item.e.nodes_start); + var node = effect.nodes.start; + var end = effect.nodes.end; - while (node !== null && node !== end) { + var dest = + next && (next.f & EFFECT_OFFSCREEN) === 0 + ? /** @type {EffectNodes} */ (next.nodes).start + : anchor; + + while (node !== null) { var next_node = /** @type {TemplateNode} */ (get_next_sibling(node)); dest.before(node); + + if (node === end) { + return; + } + node = next_node; } } /** * @param {EachState} state - * @param {EachItem | null} prev - * @param {EachItem | null} next + * @param {Effect | null} prev + * @param {Effect | null} next */ function link(state, prev, next) { if (prev === null) { - state.first = next; + state.effect.first = next; } else { prev.next = next; - prev.e.next = next && next.e; } - if (next !== null) { + if (next === null) { + state.effect.last = prev; + } else { next.prev = prev; - next.e.prev = prev && prev.e; } } diff --git a/packages/svelte/src/internal/client/dom/blocks/html.js b/packages/svelte/src/internal/client/dom/blocks/html.js index d7190abc66..1d337c1f74 100644 --- a/packages/svelte/src/internal/client/dom/blocks/html.js +++ b/packages/svelte/src/internal/client/dom/blocks/html.js @@ -54,9 +54,9 @@ export function html(node, get_value, svg = false, mathml = false, skip_warning return; } - if (effect.nodes_start !== null) { - remove_effect_dom(effect.nodes_start, /** @type {TemplateNode} */ (effect.nodes_end)); - effect.nodes_start = effect.nodes_end = null; + if (effect.nodes !== null) { + remove_effect_dom(effect.nodes.start, /** @type {TemplateNode} */ (effect.nodes.end)); + effect.nodes = null; } if (value === '') return; @@ -65,6 +65,8 @@ export function html(node, get_value, svg = false, mathml = false, skip_warning // We're deliberately not trying to repair mismatches between server and client, // as it's costly and error-prone (and it's an edge case to have a mismatch anyway) var hash = /** @type {Comment} */ (hydrate_node).data; + + /** @type {TemplateNode | null} */ var next = hydrate_next(); var last = next; @@ -73,7 +75,7 @@ export function html(node, get_value, svg = false, mathml = false, skip_warning (next.nodeType !== COMMENT_NODE || /** @type {Comment} */ (next).data !== '') ) { last = next; - next = /** @type {TemplateNode} */ (get_next_sibling(next)); + next = get_next_sibling(next); } if (next === null) { @@ -110,7 +112,7 @@ export function html(node, get_value, svg = false, mathml = false, skip_warning if (svg || mathml) { while (get_first_child(node)) { - anchor.before(/** @type {Node} */ (get_first_child(node))); + anchor.before(/** @type {TemplateNode} */ (get_first_child(node))); } } else { anchor.before(node); diff --git a/packages/svelte/src/internal/client/dom/blocks/svelte-element.js b/packages/svelte/src/internal/client/dom/blocks/svelte-element.js index 6533ff8921..47dfe7707d 100644 --- a/packages/svelte/src/internal/client/dom/blocks/svelte-element.js +++ b/packages/svelte/src/internal/client/dom/blocks/svelte-element.js @@ -1,4 +1,4 @@ -/** @import { Effect, TemplateNode } from '#client' */ +/** @import { Effect, EffectNodes, TemplateNode } from '#client' */ import { FILENAME, NAMESPACE_SVG } from '../../../../constants.js'; import { hydrate_next, @@ -10,7 +10,6 @@ import { import { create_text, get_first_child } from '../operations.js'; import { block, teardown } from '../../reactivity/effects.js'; import { set_should_intro } from '../../render.js'; -import { current_each_item, set_current_each_item } from './each.js'; import { active_effect } from '../../runtime.js'; import { component_context, dev_stack } from '../../context.js'; import { DEV } from 'esm-env'; @@ -18,6 +17,7 @@ import { EFFECT_TRANSPARENT, ELEMENT_NODE } from '#client/constants'; import { assign_nodes } from '../template.js'; import { is_raw_text_element } from '../../../../utils.js'; import { BranchManager } from './branches.js'; +import { set_animation_effect_override } from '../elements/transitions.js'; /** * @param {Comment | Element} node @@ -48,11 +48,10 @@ export function element(node, get_tag, is_svg, render_fn, get_namespace, locatio var anchor = /** @type {TemplateNode} */ (hydrating ? hydrate_node : node); /** - * The keyed `{#each ...}` item block, if any, that this element is inside. * We track this so we can set it when changing the element, allowing any * `animate:` directive to bind itself to the correct block */ - var each_item_block = current_each_item; + var parent_effect = /** @type {Effect} */ (active_effect); var branches = new BranchManager(anchor, false); @@ -67,10 +66,6 @@ export function element(node, get_tag, is_svg, render_fn, get_namespace, locatio } branches.ensure(next_tag, (anchor) => { - // See explanation of `each_item_block` above - var previous_each_item = current_each_item; - set_current_each_item(each_item_block); - if (next_tag) { element = hydrating ? /** @type {Element} */ (element) @@ -100,9 +95,9 @@ export function element(node, get_tag, is_svg, render_fn, get_namespace, locatio // If hydrating, use the existing ssr comment as the anchor so that the // inner open and close methods can pick up the existing nodes correctly - var child_anchor = /** @type {TemplateNode} */ ( - hydrating ? get_first_child(element) : element.appendChild(create_text()) - ); + var child_anchor = hydrating + ? get_first_child(element) + : element.appendChild(create_text()); if (hydrating) { if (child_anchor === null) { @@ -112,21 +107,23 @@ export function element(node, get_tag, is_svg, render_fn, get_namespace, locatio } } + set_animation_effect_override(parent_effect); + // `child_anchor` is undefined if this is a void element, but we still // need to call `render_fn` in order to run actions etc. If the element // contains children, it's a user error (which is warned on elsewhere) // and the DOM will be silently discarded render_fn(element, child_anchor); + + set_animation_effect_override(null); } // we do this after calling `render_fn` so that child effects don't override `nodes.end` - /** @type {Effect} */ (active_effect).nodes_end = element; + /** @type {Effect & { nodes: EffectNodes }} */ (active_effect).nodes.end = element; anchor.before(element); } - set_current_each_item(previous_each_item); - if (hydrating) { set_hydrate_node(anchor); } diff --git a/packages/svelte/src/internal/client/dom/blocks/svelte-head.js b/packages/svelte/src/internal/client/dom/blocks/svelte-head.js index 66d3371836..7c7eed24f7 100644 --- a/packages/svelte/src/internal/client/dom/blocks/svelte-head.js +++ b/packages/svelte/src/internal/client/dom/blocks/svelte-head.js @@ -3,22 +3,13 @@ import { hydrate_node, hydrating, set_hydrate_node, set_hydrating } from '../hyd import { create_text, get_first_child, get_next_sibling } from '../operations.js'; import { block } from '../../reactivity/effects.js'; import { COMMENT_NODE, HEAD_EFFECT } from '#client/constants'; -import { HYDRATION_START } from '../../../../constants.js'; - -/** - * @type {Node | undefined} - */ -let head_anchor; - -export function reset_head_anchor() { - head_anchor = undefined; -} /** + * @param {string} hash * @param {(anchor: Node) => void} render_fn * @returns {void} */ -export function head(render_fn) { +export function head(hash, render_fn) { // The head function may be called after the first hydration pass and ssr comment nodes may still be present, // therefore we need to skip that when we detect that we're not in hydration mode. let previous_hydrate_node = null; @@ -30,17 +21,15 @@ export function head(render_fn) { if (hydrating) { previous_hydrate_node = hydrate_node; - // There might be multiple head blocks in our app, so we need to account for each one needing independent hydration. - if (head_anchor === undefined) { - head_anchor = /** @type {TemplateNode} */ (get_first_child(document.head)); - } + var head_anchor = get_first_child(document.head); + // There might be multiple head blocks in our app, and they could have been + // rendered in an arbitrary order — find one corresponding to this component while ( head_anchor !== null && - (head_anchor.nodeType !== COMMENT_NODE || - /** @type {Comment} */ (head_anchor).data !== HYDRATION_START) + (head_anchor.nodeType !== COMMENT_NODE || /** @type {Comment} */ (head_anchor).data !== hash) ) { - head_anchor = /** @type {TemplateNode} */ (get_next_sibling(head_anchor)); + head_anchor = get_next_sibling(head_anchor); } // If we can't find an opening hydration marker, skip hydration (this can happen @@ -48,7 +37,10 @@ export function head(render_fn) { if (head_anchor === null) { set_hydrating(false); } else { - head_anchor = set_hydrate_node(/** @type {TemplateNode} */ (get_next_sibling(head_anchor))); + var start = /** @type {TemplateNode} */ (get_next_sibling(head_anchor)); + head_anchor.remove(); // in case this component is repeated + + set_hydrate_node(start); } } @@ -61,7 +53,6 @@ export function head(render_fn) { } finally { if (was_hydrating) { set_hydrating(true); - head_anchor = hydrate_node; // so that next head block starts from the correct node set_hydrate_node(/** @type {TemplateNode} */ (previous_hydrate_node)); } } diff --git a/packages/svelte/src/internal/client/dom/elements/attachments.js b/packages/svelte/src/internal/client/dom/elements/attachments.js index 4fc1280138..8a3c313ae7 100644 --- a/packages/svelte/src/internal/client/dom/elements/attachments.js +++ b/packages/svelte/src/internal/client/dom/elements/attachments.js @@ -1,5 +1,5 @@ /** @import { Effect } from '#client' */ -import { block, branch, effect, destroy_effect } from '../../reactivity/effects.js'; +import { branch, effect, destroy_effect, managed } from '../../reactivity/effects.js'; // TODO in 6.0 or 7.0, when we remove legacy mode, we can simplify this by // getting rid of the block/branch stuff and just letting the effect rip. @@ -16,7 +16,7 @@ export function attach(node, get_fn) { /** @type {Effect | null} */ var e; - block(() => { + managed(() => { if (fn !== (fn = get_fn())) { if (e) { destroy_effect(e); diff --git a/packages/svelte/src/internal/client/dom/elements/attributes.js b/packages/svelte/src/internal/client/dom/elements/attributes.js index a5f63359c9..6f1fa7391e 100644 --- a/packages/svelte/src/internal/client/dom/elements/attributes.js +++ b/packages/svelte/src/internal/client/dom/elements/attributes.js @@ -1,4 +1,4 @@ -/** @import { Effect } from '#client' */ +/** @import { Blocker, Effect } from '#client' */ import { DEV } from 'esm-env'; import { hydrating, set_hydrating } from '../hydration.js'; import { get_descriptors, get_prototype_of } from '../../../shared/utils.js'; @@ -7,7 +7,7 @@ import { add_form_reset_listener, autofocus } from './misc.js'; import * as w from '../../warnings.js'; import { LOADING_ATTR_SYMBOL } from '#client/constants'; import { queue_micro_task } from '../task.js'; -import { is_capture_event, is_delegated, normalize_attribute } from '../../../../utils.js'; +import { is_capture_event, can_delegate_event, normalize_attribute } from '../../../../utils.js'; import { active_effect, active_reaction, @@ -20,7 +20,7 @@ import { clsx } from '../../../shared/attributes.js'; import { set_class } from './class.js'; import { set_style } from './style.js'; import { ATTACHMENT_KEY, NAMESPACE_HTML, UNINITIALIZED } from '../../../../constants.js'; -import { block, branch, destroy_effect, effect } from '../../reactivity/effects.js'; +import { branch, destroy_effect, effect, managed } from '../../reactivity/effects.js'; import { init_select, select_option } from './bindings/select.js'; import { flatten } from '../../reactivity/async.js'; @@ -378,7 +378,7 @@ function set_attributes( const opts = {}; const event_handle_key = '$$' + key; let event_name = key.slice(2); - var delegated = is_delegated(event_name); + var delegated = can_delegate_event(event_name); if (is_capture_event(event_name)) { event_name = event_name.slice(0, -7); @@ -483,6 +483,7 @@ function set_attributes( * @param {(...expressions: any) => Record} fn * @param {Array<() => any>} sync * @param {Array<() => Promise>} async + * @param {Blocker[]} blockers * @param {string} [css_hash] * @param {boolean} [should_remove_defaults] * @param {boolean} [skip_warning] @@ -492,11 +493,12 @@ export function attribute_effect( fn, sync = [], async = [], + blockers = [], css_hash, should_remove_defaults = false, skip_warning = false ) { - flatten(sync, async, (values) => { + flatten(blockers, sync, async, (values) => { /** @type {Record | undefined} */ var prev = undefined; @@ -506,7 +508,7 @@ export function attribute_effect( var is_select = element.nodeName === 'SELECT'; var inited = false; - block(() => { + managed(() => { var next = fn(...values.map(get)); /** @type {Record} */ var current = set_attributes( diff --git a/packages/svelte/src/internal/client/dom/elements/bindings/this.js b/packages/svelte/src/internal/client/dom/elements/bindings/this.js index e9bbcedc6f..f2e715113f 100644 --- a/packages/svelte/src/internal/client/dom/elements/bindings/this.js +++ b/packages/svelte/src/internal/client/dom/elements/bindings/this.js @@ -38,7 +38,7 @@ export function bind_this(element_or_component = {}, update, get_value, get_part untrack(() => { if (element_or_component !== get_value(...parts)) { update(element_or_component, ...parts); - // If this is an effect rerun (cause: each block context changes), then nullfiy the binding at + // If this is an effect rerun (cause: each block context changes), then nullify the binding at // the previous position if it isn't already taken over by a different effect. if (old_parts && is_bound_this(get_value(...old_parts), element_or_component)) { update(null, ...old_parts); diff --git a/packages/svelte/src/internal/client/dom/elements/custom-element.js b/packages/svelte/src/internal/client/dom/elements/custom-element.js index 2d118bfab3..40d3d68ec0 100644 --- a/packages/svelte/src/internal/client/dom/elements/custom-element.js +++ b/packages/svelte/src/internal/client/dom/elements/custom-element.js @@ -35,18 +35,23 @@ if (typeof HTMLElement === 'function') { $$l_u = new Map(); /** @type {any} The managed render effect for reflecting attributes */ $$me; + /** @type {ShadowRoot | null} The ShadowRoot of the custom element */ + $$shadowRoot = null; /** * @param {*} $$componentCtor * @param {*} $$slots - * @param {*} use_shadow_dom + * @param {ShadowRootInit | undefined} shadow_root_init */ - constructor($$componentCtor, $$slots, use_shadow_dom) { + constructor($$componentCtor, $$slots, shadow_root_init) { super(); this.$$ctor = $$componentCtor; this.$$s = $$slots; - if (use_shadow_dom) { - this.attachShadow({ mode: 'open' }); + + if (shadow_root_init) { + // We need to store the reference to shadow root, because `closed` shadow root cannot be + // accessed with `this.shadowRoot`. + this.$$shadowRoot = this.attachShadow(shadow_root_init); } } @@ -136,7 +141,7 @@ if (typeof HTMLElement === 'function') { } this.$$c = createClassComponent({ component: this.$$ctor, - target: this.shadowRoot || this, + target: this.$$shadowRoot || this, props: { ...this.$$d, $$slots, @@ -277,7 +282,7 @@ function get_custom_elements_slots(element) { * @param {Record} props_definition The props to observe * @param {string[]} slots The slots to create * @param {string[]} exports Explicitly exported values, other than props - * @param {boolean} use_shadow_dom Whether to use shadow DOM + * @param {ShadowRootInit | undefined} shadow_root_init Options passed to shadow DOM constructor * @param {(ce: new () => HTMLElement) => new () => HTMLElement} [extend] */ export function create_custom_element( @@ -285,12 +290,12 @@ export function create_custom_element( props_definition, slots, exports, - use_shadow_dom, + shadow_root_init, extend ) { let Class = class extends SvelteElement { constructor() { - super(Component, slots, use_shadow_dom); + super(Component, slots, shadow_root_init); this.$$p_d = props_definition; } static get observedAttributes() { diff --git a/packages/svelte/src/internal/client/dom/elements/customizable-select.js b/packages/svelte/src/internal/client/dom/elements/customizable-select.js new file mode 100644 index 0000000000..1858565d2a --- /dev/null +++ b/packages/svelte/src/internal/client/dom/elements/customizable-select.js @@ -0,0 +1,98 @@ +import { hydrating, reset, set_hydrate_node, set_hydrating } from '../hydration.js'; +import { create_comment } from '../operations.js'; +import { attach } from './attachments.js'; + +/** @type {boolean | null} */ +let supported = null; + +/** + * Checks if the browser supports rich HTML content inside `'; + supported = /** @type {Element} */ (select.firstChild)?.firstChild?.nodeType === 1; + } + + return supported; +} + +/** + * + * @param {HTMLElement} element + * @param {(new_element: HTMLElement) => void} update_element + */ +export function selectedcontent(element, update_element) { + // if it's not supported no need for special logic + if (!is_supported()) return; + + // we use the attach function directly just to make sure is executed when is mounted to the dom + attach(element, () => () => { + const select = element.closest('select'); + if (!select) return; + + const observer = new MutationObserver((entries) => { + var selected = false; + + for (const entry of entries) { + if (entry.target === element) { + // the `` already changed, no need to replace it + return; + } + + // if the changes doesn't include the selected ``); + renderer.#out.push(`>${body}${is_rich ? '' : ''}`); // super edge case, but may as well handle it if (head) { @@ -320,13 +379,13 @@ export class Renderer { * Takes a component and returns an object with `body` and `head` properties on it, which you can use to populate the HTML when server-rendering your app. * @template {Record} Props * @param {Component} component - * @param {{ props?: Omit; context?: Map; idPrefix?: string }} [options] + * @param {{ props?: Omit; context?: Map; idPrefix?: string; csp?: Csp }} [options] * @returns {RenderOutput} */ static render(component, options = {}) { /** @type {AccumulatedContent | undefined} */ let sync; - /** @type {Promise | undefined} */ + /** @type {Promise | undefined} */ let async; const result = /** @type {RenderOutput} */ ({}); @@ -348,6 +407,11 @@ export class Renderer { return (sync ??= Renderer.#render(component, options)).body; } }, + hashes: { + value: { + script: '' + } + }, then: { value: /** @@ -364,11 +428,14 @@ export class Renderer { const user_result = onfulfilled({ head: result.head, body: result.body, - html: result.body + html: result.body, + hashes: { script: [] } }); return Promise.resolve(user_result); } - async ??= Renderer.#render_async(component, options); + async ??= init_render_context().then(() => + with_render_context(() => Renderer.#render_async(component, options)) + ); return async.then((result) => { Object.defineProperty(result, 'html', { // eslint-disable-next-line getter-return @@ -386,7 +453,7 @@ export class Renderer { } /** - * Collect all of the `onDestroy` callbacks regsitered during rendering. In an async context, this is only safe to call + * Collect all of the `onDestroy` callbacks registered during rendering. In an async context, this is only safe to call * after awaiting `collect_async`. * * Child renderers are "porous" and don't affect execution order, but component body renderers @@ -456,19 +523,23 @@ export class Renderer { * * @template {Record} Props * @param {Component} component - * @param {{ props?: Omit; context?: Map; idPrefix?: string }} options - * @returns {Promise} + * @param {{ props?: Omit; context?: Map; idPrefix?: string; csp?: Csp }} options + * @returns {Promise} */ static async #render_async(component, options) { - var previous_context = ssr_context; + const previous_context = ssr_context; + try { const renderer = Renderer.#open_render('async', component, options); - const content = await renderer.#collect_content_async(); + const hydratables = await renderer.#collect_hydratables(); + if (hydratables !== null) { + content.head = hydratables + content.head; + } return Renderer.#close_render(content, renderer); } finally { - abort(); set_ssr_context(previous_context); + abort(); } } @@ -509,16 +580,33 @@ export class Renderer { return content; } + async #collect_hydratables() { + const ctx = get_render_context().hydratable; + + for (const [_, key] of ctx.unresolved_promises) { + // this is a problem -- it means we've finished the render but we're still waiting on a promise to resolve so we can + // serialize it, so we're blocking the response on useless content. + w.unresolved_hydratable(key, ctx.lookup.get(key)?.stack ?? ''); + } + + for (const comparison of ctx.comparisons) { + // these reject if there's a mismatch + await comparison; + } + + return await this.#hydratable_block(ctx); + } + /** * @template {Record} Props * @param {'sync' | 'async'} mode * @param {import('svelte').Component} component - * @param {{ props?: Omit; context?: Map; idPrefix?: string }} options + * @param {{ props?: Omit; context?: Map; idPrefix?: string; csp?: Csp }} options * @returns {Renderer} */ static #open_render(mode, component, options) { const renderer = new Renderer( - new SSRState(mode, options.idPrefix ? options.idPrefix + '-' : '') + new SSRState(mode, options.idPrefix ? options.idPrefix + '-' : '', options.csp) ); renderer.push(BLOCK_OPEN); @@ -544,6 +632,7 @@ export class Renderer { /** * @param {AccumulatedContent} content * @param {Renderer} renderer + * @returns {AccumulatedContent & { hashes: { script: Sha256Source[] } }} */ static #close_render(content, renderer) { for (const cleanup of renderer.#collect_on_destroy()) { @@ -559,12 +648,71 @@ export class Renderer { return { head, - body + body, + hashes: { + script: renderer.global.csp.script_hashes + } }; } + + /** + * @param {HydratableContext} ctx + */ + async #hydratable_block(ctx) { + if (ctx.lookup.size === 0) { + return null; + } + + let entries = []; + let has_promises = false; + + for (const [k, v] of ctx.lookup) { + if (v.promises) { + has_promises = true; + for (const p of v.promises) await p; + } + + entries.push(`[${devalue.uneval(k)},${v.serialized}]`); + } + + let prelude = `const h = (window.__svelte ??= {}).h ??= new Map();`; + + if (has_promises) { + prelude = `const r = (v) => Promise.resolve(v); + ${prelude}`; + } + + const body = ` + { + ${prelude} + + for (const [k, v] of [ + ${entries.join(',\n\t\t\t\t\t')} + ]) { + h.set(k, v); + } + } + `; + + let csp_attr = ''; + if (this.global.csp.nonce) { + csp_attr = ` nonce="${this.global.csp.nonce}"`; + } else if (this.global.csp.hash) { + // note to future selves: this doesn't need to be optimized with a Map + // because the it's impossible for identical data to occur multiple times in a single render + // (this would require the same hydratable key:value pair to be serialized multiple times) + const hash = await sha256(body); + this.global.csp.script_hashes.push(`sha256-${hash}`); + } + + return `\n\t\t${body}`; + } } export class SSRState { + /** @readonly @type {Csp & { script_hashes: Sha256Source[] }} */ + csp; + /** @readonly @type {'sync' | 'async'} */ mode; @@ -579,10 +727,12 @@ export class SSRState { /** * @param {'sync' | 'async'} mode - * @param {string} [id_prefix] + * @param {string} id_prefix + * @param {Csp} csp */ - constructor(mode, id_prefix = '') { + constructor(mode, id_prefix = '', csp = { hash: false }) { this.mode = mode; + this.csp = { ...csp, script_hashes: [] }; let uid = 1; this.uid = () => `${id_prefix}s${uid++}`; diff --git a/packages/svelte/src/internal/server/types.d.ts b/packages/svelte/src/internal/server/types.d.ts index 53cefabc69..ea6282c176 100644 --- a/packages/svelte/src/internal/server/types.d.ts +++ b/packages/svelte/src/internal/server/types.d.ts @@ -1,3 +1,4 @@ +import type { MaybePromise } from '#shared'; import type { Element } from './dev'; import type { Renderer } from './renderer'; @@ -14,6 +15,28 @@ export interface SSRContext { element?: Element; } +export type Csp = { nonce?: string; hash?: boolean }; + +export interface HydratableLookupEntry { + value: unknown; + serialized: string; + promises?: Array>; + /** dev-only */ + stack?: string; +} + +export interface HydratableContext { + lookup: Map; + comparisons: Promise[]; + unresolved_promises: Map, string>; +} + +export interface RenderContext { + hydratable: HydratableContext; +} + +export type Sha256Source = `sha256-${string}`; + export interface SyncRenderOutput { /** HTML that goes into the `` */ head: string; @@ -21,6 +44,9 @@ export interface SyncRenderOutput { html: string; /** HTML that goes somewhere into the `` */ body: string; + hashes: { + script: Sha256Source[]; + }; } export type RenderOutput = SyncRenderOutput & PromiseLike; diff --git a/packages/svelte/src/internal/server/warnings.js b/packages/svelte/src/internal/server/warnings.js index d4ee7a86c2..fc44a086af 100644 --- a/packages/svelte/src/internal/server/warnings.js +++ b/packages/svelte/src/internal/server/warnings.js @@ -3,4 +3,27 @@ import { DEV } from 'esm-env'; var bold = 'font-weight: bold'; -var normal = 'font-weight: normal'; \ No newline at end of file +var normal = 'font-weight: normal'; + +/** + * A `hydratable` value with key `%key%` was created, but at least part of it was not used during the render. + * + * The `hydratable` was initialized in: + * %stack% + * @param {string} key + * @param {string} stack + */ +export function unresolved_hydratable(key, stack) { + if (DEV) { + console.warn( + `%c[svelte] unresolved_hydratable\n%cA \`hydratable\` value with key \`${key}\` was created, but at least part of it was not used during the render. + +The \`hydratable\` was initialized in: +${stack}\nhttps://svelte.dev/e/unresolved_hydratable`, + bold, + normal + ); + } else { + console.warn(`https://svelte.dev/e/unresolved_hydratable`); + } +} \ No newline at end of file diff --git a/packages/svelte/src/internal/shared/dev.js b/packages/svelte/src/internal/shared/dev.js new file mode 100644 index 0000000000..aadb3c7e6d --- /dev/null +++ b/packages/svelte/src/internal/shared/dev.js @@ -0,0 +1,65 @@ +import { define_property } from './utils.js'; + +/** + * @param {string} label + * @returns {Error & { stack: string } | null} + */ +export function get_error(label) { + const error = new Error(); + const stack = get_stack(); + + if (stack.length === 0) { + return null; + } + + stack.unshift('\n'); + + define_property(error, 'stack', { + value: stack.join('\n') + }); + + define_property(error, 'name', { + value: label + }); + + return /** @type {Error & { stack: string }} */ (error); +} + +/** + * @returns {string[]} + */ +export function get_stack() { + // @ts-ignore - doesn't exist everywhere + const limit = Error.stackTraceLimit; + // @ts-ignore - doesn't exist everywhere + Error.stackTraceLimit = Infinity; + const stack = new Error().stack; + // @ts-ignore - doesn't exist everywhere + Error.stackTraceLimit = limit; + + if (!stack) return []; + + const lines = stack.split('\n'); + const new_lines = []; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + const posixified = line.replaceAll('\\', '/'); + + if (line.trim() === 'Error') { + continue; + } + + if (line.includes('validate_each_keys')) { + return []; + } + + if (posixified.includes('svelte/src/internal') || posixified.includes('node_modules/.vite')) { + continue; + } + + new_lines.push(line); + } + + return new_lines; +} diff --git a/packages/svelte/src/internal/shared/errors.js b/packages/svelte/src/internal/shared/errors.js index 669cdd96a7..b13a65b598 100644 --- a/packages/svelte/src/internal/shared/errors.js +++ b/packages/svelte/src/internal/shared/errors.js @@ -2,6 +2,23 @@ import { DEV } from 'esm-env'; +/** + * Cannot use `%name%(...)` unless the `experimental.async` compiler option is `true` + * @param {string} name + * @returns {never} + */ +export function experimental_async_required(name) { + if (DEV) { + const error = new Error(`experimental_async_required\nCannot use \`${name}(...)\` unless the \`experimental.async\` compiler option is \`true\`\nhttps://svelte.dev/e/experimental_async_required`); + + error.name = 'Svelte error'; + + throw error; + } else { + throw new Error(`https://svelte.dev/e/experimental_async_required`); + } +} + /** * Cannot use `{@render children(...)}` if the parent component uses `let:` directives. Consider using a named snippet instead * @returns {never} diff --git a/packages/svelte/src/internal/shared/types.d.ts b/packages/svelte/src/internal/shared/types.d.ts index 4deeb76b2f..3374d7bc16 100644 --- a/packages/svelte/src/internal/shared/types.d.ts +++ b/packages/svelte/src/internal/shared/types.d.ts @@ -8,3 +8,5 @@ export type Getters = { }; export type Snapshot = ReturnType>; + +export type MaybePromise = T | Promise; diff --git a/packages/svelte/src/internal/shared/utils.js b/packages/svelte/src/internal/shared/utils.js index 10f8597520..771f6b345c 100644 --- a/packages/svelte/src/internal/shared/utils.js +++ b/packages/svelte/src/internal/shared/utils.js @@ -2,6 +2,7 @@ // to de-opt (this occurs often when using popular extensions). export var is_array = Array.isArray; export var index_of = Array.prototype.indexOf; +export var includes = Array.prototype.includes; export var array_from = Array.from; export var object_keys = Object.keys; export var define_property = Object.defineProperty; @@ -48,7 +49,7 @@ export function run_all(arr) { /** * TODO replace with Promise.withResolvers once supported widely enough - * @template T + * @template [T=void] */ export function deferred() { /** @type {(value: T) => void} */ diff --git a/packages/svelte/src/legacy/legacy-client.js b/packages/svelte/src/legacy/legacy-client.js index d4a053d1aa..ec90d2312c 100644 --- a/packages/svelte/src/legacy/legacy-client.js +++ b/packages/svelte/src/legacy/legacy-client.js @@ -3,7 +3,7 @@ import { DIRTY, LEGACY_PROPS, MAYBE_DIRTY } from '../internal/client/constants.j import { user_pre_effect } from '../internal/client/reactivity/effects.js'; import { mutable_source, set } from '../internal/client/reactivity/sources.js'; import { hydrate, mount, unmount } from '../internal/client/render.js'; -import { active_effect, get, set_signal_status } from '../internal/client/runtime.js'; +import { active_effect, get } from '../internal/client/runtime.js'; import { flushSync } from '../internal/client/reactivity/batch.js'; import { define_property, is_array } from '../internal/shared/utils.js'; import * as e from '../internal/client/errors.js'; @@ -12,6 +12,7 @@ import { DEV } from 'esm-env'; import { FILENAME } from '../constants.js'; import { component_context, dev_current_component_function } from '../internal/client/context.js'; import { async_mode_flag } from '../internal/flags/index.js'; +import { set_signal_status } from '../internal/client/reactivity/status.js'; /** * Takes the same options as a Svelte 4 component and the component function and returns a Svelte 4 compatible component. diff --git a/packages/svelte/src/legacy/legacy-server.js b/packages/svelte/src/legacy/legacy-server.js index a50d961751..05b329bea1 100644 --- a/packages/svelte/src/legacy/legacy-server.js +++ b/packages/svelte/src/legacy/legacy-server.js @@ -1,14 +1,14 @@ /** @import { SvelteComponent } from '../index.js' */ +/** @import { Csp } from '#server' */ import { asClassComponent as as_class_component, createClassComponent } from './legacy-client.js'; import { render } from '../internal/server/index.js'; import { async_mode_flag } from '../internal/flags/index.js'; -import * as w from '../internal/server/warnings.js'; // By having this as a separate entry point for server environments, we save the client bundle from having to include the server runtime export { createClassComponent }; -/** @typedef {{ head: string, html: string, css: { code: string, map: null }}} LegacyRenderResult */ +/** @typedef {{ head: string, html: string, css: { code: string, map: null }; hashes?: { script: `sha256-${string}`[] } }} LegacyRenderResult */ /** * Takes a Svelte 5 component and returns a Svelte 4 compatible component constructor. @@ -25,10 +25,10 @@ export { createClassComponent }; */ export function asClassComponent(component) { const component_constructor = as_class_component(component); - /** @type {(props?: {}, opts?: { $$slots?: {}; context?: Map; }) => LegacyRenderResult & PromiseLike } */ - const _render = (props, { context } = {}) => { + /** @type {(props?: {}, opts?: { $$slots?: {}; context?: Map; csp?: Csp }) => LegacyRenderResult & PromiseLike } */ + const _render = (props, { context, csp } = {}) => { // @ts-expect-error the typings are off, but this will work if the component is compiled in SSR mode - const result = render(component, { props, context }); + const result = render(component, { props, context, csp }); const munged = Object.defineProperties( /** @type {LegacyRenderResult & PromiseLike} */ ({}), @@ -65,7 +65,8 @@ export function asClassComponent(component) { return onfulfilled({ css: munged.css, head: result.head, - html: result.body + html: result.body, + hashes: result.hashes }); }, onrejected); } diff --git a/packages/svelte/src/server/index.d.ts b/packages/svelte/src/server/index.d.ts index d5a3b813e6..f54bd5a5ca 100644 --- a/packages/svelte/src/server/index.d.ts +++ b/packages/svelte/src/server/index.d.ts @@ -1,4 +1,4 @@ -import type { RenderOutput } from '#server'; +import type { Csp, RenderOutput } from '#server'; import type { ComponentProps, Component, SvelteComponent, ComponentType } from 'svelte'; /** @@ -16,6 +16,7 @@ export function render< props?: Omit; context?: Map; idPrefix?: string; + csp?: Csp; } ] : [ @@ -24,6 +25,7 @@ export function render< props: Omit; context?: Map; idPrefix?: string; + csp?: Csp; } ] ): RenderOutput; diff --git a/packages/svelte/src/utils.js b/packages/svelte/src/utils.js index a54a421418..d63d4ff801 100644 --- a/packages/svelte/src/utils.js +++ b/packages/svelte/src/utils.js @@ -137,7 +137,7 @@ const DELEGATED_EVENTS = [ * Returns `true` if `event_name` is a delegated event * @param {string} event_name */ -export function is_delegated(event_name) { +export function can_delegate_event(event_name) { return DELEGATED_EVENTS.includes(event_name); } diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index e33d22d4c4..4338ba83ae 100644 --- a/packages/svelte/src/version.js +++ b/packages/svelte/src/version.js @@ -4,5 +4,5 @@ * The current version, as set in package.json. * @type {string} */ -export const VERSION = '5.41.1'; +export const VERSION = '5.48.5'; export const PUBLIC_VERSION = '5'; diff --git a/packages/svelte/tests/README.md b/packages/svelte/tests/README.md index 6dde66f6cb..9e450a7176 100644 --- a/packages/svelte/tests/README.md +++ b/packages/svelte/tests/README.md @@ -1,6 +1,6 @@ # Test repo -This repo tries to migrate as many tests from the currente Svelte project over to test against the new compiler/runtime. +This repo tries to migrate as many tests from the current Svelte project over to test against the new compiler/runtime. ## Differences to the old test suite diff --git a/packages/svelte/tests/css-parse.test.ts b/packages/svelte/tests/css-parse.test.ts new file mode 100644 index 0000000000..4d8ef8601b --- /dev/null +++ b/packages/svelte/tests/css-parse.test.ts @@ -0,0 +1,153 @@ +import { assert, describe, it } from 'vitest'; +import { parseCss } from 'svelte/compiler'; + +describe('parseCss', () => { + it('parses a simple rule', () => { + const ast = parseCss('div { color: red; }'); + assert.equal(ast.type, 'StyleSheet'); + assert.equal(ast.children.length, 1); + assert.equal(ast.children[0].type, 'Rule'); + }); + + it('parses at-rules', () => { + const ast = parseCss('@media (min-width: 800px) { div { color: red; } }'); + assert.equal(ast.children.length, 1); + assert.equal(ast.children[0].type, 'Atrule'); + if (ast.children[0].type === 'Atrule') { + assert.equal(ast.children[0].name, 'media'); + } + }); + + it('parses @import', () => { + const ast = parseCss("@import 'foo.css';"); + assert.equal(ast.children.length, 1); + assert.equal(ast.children[0].type, 'Atrule'); + if (ast.children[0].type === 'Atrule') { + assert.equal(ast.children[0].name, 'import'); + assert.equal(ast.children[0].block, null); + } + }); + + it('parses multiple rules', () => { + const ast = parseCss('div { color: red; } span { color: blue; }'); + assert.equal(ast.children.length, 2); + }); + + it('has correct start/end positions', () => { + const ast = parseCss('div { color: red; }'); + assert.equal(ast.start, 0); + assert.equal(ast.end, 19); + }); + + it('strips BOM', () => { + const ast = parseCss('\uFEFFdiv { color: red; }'); + assert.equal(ast.start, 0); + assert.equal(ast.end, 19); + }); + + it('parses nested rules', () => { + const ast = parseCss('div { color: red; span { color: blue; } }'); + assert.equal(ast.children.length, 1); + const rule = ast.children[0]; + assert.equal(rule.type, 'Rule'); + if (rule.type === 'Rule') { + assert.equal(rule.block.children.length, 2); // declaration + nested rule + } + }); + + it('parses empty stylesheet', () => { + const ast = parseCss(''); + assert.equal(ast.type, 'StyleSheet'); + assert.equal(ast.children.length, 0); + assert.equal(ast.start, 0); + assert.equal(ast.end, 0); + }); + + it('parses whitespace-only stylesheet', () => { + const ast = parseCss(' \n\t '); + assert.equal(ast.children.length, 0); + }); + + it('parses comments', () => { + const ast = parseCss('/* comment */ div { color: red; }'); + assert.equal(ast.children.length, 1); + assert.equal(ast.children[0].type, 'Rule'); + }); + + it('parses complex selectors', () => { + const ast = parseCss('div > span + p ~ a { color: red; }'); + assert.equal(ast.children.length, 1); + const rule = ast.children[0]; + if (rule.type === 'Rule') { + assert.equal(rule.prelude.type, 'SelectorList'); + assert.equal(rule.prelude.children.length, 1); + // div > span + p ~ a has 4 relative selectors + assert.equal(rule.prelude.children[0].children.length, 4); + } + }); + + it('parses pseudo-classes and pseudo-elements', () => { + const ast = parseCss('div:hover::before { color: red; }'); + assert.equal(ast.children.length, 1); + const rule = ast.children[0]; + if (rule.type === 'Rule') { + const selectors = rule.prelude.children[0].children[0].selectors; + assert.equal(selectors.length, 3); // div, :hover, ::before + assert.equal(selectors[0].type, 'TypeSelector'); + assert.equal(selectors[1].type, 'PseudoClassSelector'); + assert.equal(selectors[2].type, 'PseudoElementSelector'); + } + }); + + it('parses @keyframes', () => { + const ast = parseCss('@keyframes fade { from { opacity: 0; } to { opacity: 1; } }'); + assert.equal(ast.children.length, 1); + assert.equal(ast.children[0].type, 'Atrule'); + if (ast.children[0].type === 'Atrule') { + assert.equal(ast.children[0].name, 'keyframes'); + assert.notEqual(ast.children[0].block, null); + } + }); + + it('parses class and id selectors', () => { + const ast = parseCss('.foo#bar { color: red; }'); + assert.equal(ast.children.length, 1); + const rule = ast.children[0]; + if (rule.type === 'Rule') { + const selectors = rule.prelude.children[0].children[0].selectors; + assert.equal(selectors.length, 2); + assert.equal(selectors[0].type, 'ClassSelector'); + assert.equal(selectors[1].type, 'IdSelector'); + } + }); + + it('parses attribute selectors', () => { + const ast = parseCss('[data-foo="bar"] { color: red; }'); + assert.equal(ast.children.length, 1); + const rule = ast.children[0]; + if (rule.type === 'Rule') { + const selectors = rule.prelude.children[0].children[0].selectors; + assert.equal(selectors.length, 1); + assert.equal(selectors[0].type, 'AttributeSelector'); + if (selectors[0].type === 'AttributeSelector') { + assert.equal(selectors[0].name, 'data-foo'); + assert.equal(selectors[0].value, 'bar'); + } + } + }); + + it('parses escaped characters', () => { + const ast = parseCss("div { background: url('./example.png?\\''); }"); + assert.equal(ast.type, 'StyleSheet'); + assert.equal(ast.children.length, 1); + const rule = ast.children[0]; + assert.equal(rule.type, 'Rule'); + if (rule.type === 'Rule') { + const declaration = rule.block.children[0]; + assert.equal(declaration.type, 'Declaration'); + if (declaration.type === 'Declaration') { + assert.equal(declaration.value, "url('./example.png?\\'')"); + } + } + }); +}); diff --git a/packages/svelte/tests/css/samples/selectedcontent/expected.css b/packages/svelte/tests/css/samples/selectedcontent/expected.css new file mode 100644 index 0000000000..87b4e04de7 --- /dev/null +++ b/packages/svelte/tests/css/samples/selectedcontent/expected.css @@ -0,0 +1,44 @@ + + select.svelte-xyz, + .svelte-xyz::picker(select) { + appearance: base-select; + } + selectedcontent.svelte-xyz b:where(.svelte-xyz){ + color: red; + } + e.svelte-xyz{ + selectedcontent:where(.svelte-xyz) &{ + color: green; + } + } + select.svelte-xyz > button:where(.svelte-xyz) > selectedcontent:where(.svelte-xyz) > b:where(.svelte-xyz) { + color: blue; + } + + select.svelte-xyz > button:where(.svelte-xyz) > selectedcontent:where(.svelte-xyz) i:where(.svelte-xyz) { + color: blue; + } + + selectedcontent.svelte-xyz:has(b:where(.svelte-xyz)){ + background-color: rebeccapurple; + } + + selectedcontent.svelte-xyz:has(i:where(.svelte-xyz)){ + background-color: rebeccapurple; + } + + option.svelte-xyz > b:where(.svelte-xyz){ + color: orange; + } + + option.svelte-xyz b:where(.svelte-xyz){ + color: #ff3e00; + } + + option.svelte-xyz > b:where(.svelte-xyz) > i:where(.svelte-xyz){ + text-decoration: underline; + } + + option.svelte-xyz i:where(.svelte-xyz){ + text-decoration: dashed; + } diff --git a/packages/svelte/tests/css/samples/selectedcontent/input.svelte b/packages/svelte/tests/css/samples/selectedcontent/input.svelte new file mode 100644 index 0000000000..b8305551d7 --- /dev/null +++ b/packages/svelte/tests/css/samples/selectedcontent/input.svelte @@ -0,0 +1,53 @@ + + + \ No newline at end of file diff --git a/packages/svelte/tests/helpers.js b/packages/svelte/tests/helpers.js index bf708878a3..d0ec8b6e44 100644 --- a/packages/svelte/tests/helpers.js +++ b/packages/svelte/tests/helpers.js @@ -201,7 +201,15 @@ export const async_mode = process.env.SVELTE_NO_ASYNC !== 'true'; * @param {any[]} logs */ export function normalise_inspect_logs(logs) { - return logs.map((log) => { + /** @type {string[]} */ + const normalised = []; + + for (const log of logs) { + if (log === 'stack trace') { + // ignore `console.group('stack trace')` in default `$inspect(...)` output + continue; + } + if (log instanceof Error) { const last_line = log.stack ?.trim() @@ -210,11 +218,13 @@ export function normalise_inspect_logs(logs) { const match = last_line && /(at .+) /.exec(last_line); - return match && match[1]; + if (match) normalised.push(match[1]); + } else { + normalised.push(log); } + } - return log; - }); + return normalised; } /** 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/test.ts b/packages/svelte/tests/hydration/test.ts index 70d5c5d072..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,15 +131,12 @@ const { test, run } = suite(async (config, cwd) => { flushSync(); - const normalize = (string: string) => - string.trim().replaceAll('\r\n', '\n').replaceAll('/>', '>'); - 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/manual/each-stress-test/main.svelte b/packages/svelte/tests/manual/each-stress-test/main.svelte new file mode 100644 index 0000000000..cb69612844 --- /dev/null +++ b/packages/svelte/tests/manual/each-stress-test/main.svelte @@ -0,0 +1,194 @@ + + +

each block stress test

+ + + + + +
+ random + + + +
+ +
+ presets + + {#each presets as preset, index} + + {/each} +
+ +
{ + e.preventDefault(); + test(e.currentTarget.querySelector('input').value); +}}> +
+ input + +
+
+ +
+ {#each list as c (c)} + ({c}:{n}) + {:else} + (fallback) + {/each} +
+ +{#if error} +

{error}

+{/if} + + diff --git a/packages/svelte/tests/migrate/samples/self-closing-named-slot/input.svelte b/packages/svelte/tests/migrate/samples/self-closing-named-slot/input.svelte new file mode 100644 index 0000000000..33ca4e3b9f --- /dev/null +++ b/packages/svelte/tests/migrate/samples/self-closing-named-slot/input.svelte @@ -0,0 +1,3 @@ + +
+ \ No newline at end of file diff --git a/packages/svelte/tests/migrate/samples/self-closing-named-slot/output.svelte b/packages/svelte/tests/migrate/samples/self-closing-named-slot/output.svelte new file mode 100644 index 0000000000..6655bc9dd2 --- /dev/null +++ b/packages/svelte/tests/migrate/samples/self-closing-named-slot/output.svelte @@ -0,0 +1,5 @@ + + {#snippet test()} +
+ {/snippet} +
\ No newline at end of file diff --git a/packages/svelte/tests/parser-legacy/samples/action-duplicate/output.json b/packages/svelte/tests/parser-legacy/samples/action-duplicate/output.json index c6af77a47b..9a171547fe 100644 --- a/packages/svelte/tests/parser-legacy/samples/action-duplicate/output.json +++ b/packages/svelte/tests/parser-legacy/samples/action-duplicate/output.json @@ -15,6 +15,18 @@ "end": 20, "type": "Action", "name": "autofocus", + "name_loc": { + "start": { + "line": 1, + "column": 7, + "character": 7 + }, + "end": { + "line": 1, + "column": 20, + "character": 20 + } + }, "expression": null, "modifiers": [] }, @@ -23,6 +35,18 @@ "end": 34, "type": "Action", "name": "autofocus", + "name_loc": { + "start": { + "line": 1, + "column": 21, + "character": 21 + }, + "end": { + "line": 1, + "column": 34, + "character": 34 + } + }, "expression": null, "modifiers": [] } diff --git a/packages/svelte/tests/parser-legacy/samples/action-with-call/output.json b/packages/svelte/tests/parser-legacy/samples/action-with-call/output.json index a10d4eccf0..f3242eba5e 100644 --- a/packages/svelte/tests/parser-legacy/samples/action-with-call/output.json +++ b/packages/svelte/tests/parser-legacy/samples/action-with-call/output.json @@ -15,6 +15,18 @@ "end": 39, "type": "Action", "name": "tooltip", + "name_loc": { + "start": { + "line": 1, + "column": 7, + "character": 7 + }, + "end": { + "line": 1, + "column": 18, + "character": 18 + } + }, "expression": { "type": "CallExpression", "start": 21, diff --git a/packages/svelte/tests/parser-legacy/samples/action-with-identifier/output.json b/packages/svelte/tests/parser-legacy/samples/action-with-identifier/output.json index e9a3e7e5da..8f76afc829 100644 --- a/packages/svelte/tests/parser-legacy/samples/action-with-identifier/output.json +++ b/packages/svelte/tests/parser-legacy/samples/action-with-identifier/output.json @@ -15,6 +15,18 @@ "end": 28, "type": "Action", "name": "tooltip", + "name_loc": { + "start": { + "line": 1, + "column": 7, + "character": 7 + }, + "end": { + "line": 1, + "column": 18, + "character": 18 + } + }, "expression": { "type": "Identifier", "start": 20, diff --git a/packages/svelte/tests/parser-legacy/samples/action-with-literal/output.json b/packages/svelte/tests/parser-legacy/samples/action-with-literal/output.json index 94b60b9e5d..b6c4f2690d 100644 --- a/packages/svelte/tests/parser-legacy/samples/action-with-literal/output.json +++ b/packages/svelte/tests/parser-legacy/samples/action-with-literal/output.json @@ -15,6 +15,18 @@ "end": 36, "type": "Action", "name": "tooltip", + "name_loc": { + "start": { + "line": 1, + "column": 7, + "character": 7 + }, + "end": { + "line": 1, + "column": 18, + "character": 18 + } + }, "expression": { "type": "Literal", "start": 21, diff --git a/packages/svelte/tests/parser-legacy/samples/action/output.json b/packages/svelte/tests/parser-legacy/samples/action/output.json index f241c81a93..a271778985 100644 --- a/packages/svelte/tests/parser-legacy/samples/action/output.json +++ b/packages/svelte/tests/parser-legacy/samples/action/output.json @@ -15,6 +15,18 @@ "end": 20, "type": "Action", "name": "autofocus", + "name_loc": { + "start": { + "line": 1, + "column": 7, + "character": 7 + }, + "end": { + "line": 1, + "column": 20, + "character": 20 + } + }, "expression": null, "modifiers": [] } diff --git a/packages/svelte/tests/parser-legacy/samples/animation/output.json b/packages/svelte/tests/parser-legacy/samples/animation/output.json index bf4b43b875..262bb17d9a 100644 --- a/packages/svelte/tests/parser-legacy/samples/animation/output.json +++ b/packages/svelte/tests/parser-legacy/samples/animation/output.json @@ -20,6 +20,18 @@ "end": 50, "type": "Animation", "name": "flip", + "name_loc": { + "start": { + "line": 2, + "column": 6, + "character": 38 + }, + "end": { + "line": 2, + "column": 18, + "character": 50 + } + }, "expression": null, "modifiers": [] } @@ -39,6 +51,7 @@ "type": "Identifier", "name": "thing", "start": 17, + "end": 22, "loc": { "start": { "line": 1, @@ -50,8 +63,7 @@ "column": 22, "character": 22 } - }, - "end": 22 + } }, "expression": { "type": "Identifier", diff --git a/packages/svelte/tests/parser-legacy/samples/attribute-class-directive/output.json b/packages/svelte/tests/parser-legacy/samples/attribute-class-directive/output.json index 3cd54b6647..bbf9c12ceb 100644 --- a/packages/svelte/tests/parser-legacy/samples/attribute-class-directive/output.json +++ b/packages/svelte/tests/parser-legacy/samples/attribute-class-directive/output.json @@ -15,6 +15,18 @@ "end": 22, "type": "Class", "name": "foo", + "name_loc": { + "start": { + "line": 1, + "column": 5, + "character": 5 + }, + "end": { + "line": 1, + "column": 14, + "character": 14 + } + }, "expression": { "type": "Identifier", "start": 16, diff --git a/packages/svelte/tests/parser-legacy/samples/attribute-containing-solidus/output.json b/packages/svelte/tests/parser-legacy/samples/attribute-containing-solidus/output.json index 2c63b3a43d..c1bcbbaf5e 100644 --- a/packages/svelte/tests/parser-legacy/samples/attribute-containing-solidus/output.json +++ b/packages/svelte/tests/parser-legacy/samples/attribute-containing-solidus/output.json @@ -15,6 +15,18 @@ "start": 3, "end": 30, "name": "href", + "name_loc": { + "start": { + "line": 1, + "column": 3, + "character": 3 + }, + "end": { + "line": 1, + "column": 7, + "character": 7 + } + }, "value": [ { "start": 8, diff --git a/packages/svelte/tests/parser-legacy/samples/attribute-curly-bracket/output.json b/packages/svelte/tests/parser-legacy/samples/attribute-curly-bracket/output.json index 2453dc9e0a..d84b63745c 100644 --- a/packages/svelte/tests/parser-legacy/samples/attribute-curly-bracket/output.json +++ b/packages/svelte/tests/parser-legacy/samples/attribute-curly-bracket/output.json @@ -15,6 +15,18 @@ "start": 7, "end": 15, "name": "foo", + "name_loc": { + "start": { + "line": 1, + "column": 7, + "character": 7 + }, + "end": { + "line": 1, + "column": 10, + "character": 10 + } + }, "value": [ { "start": 11, diff --git a/packages/svelte/tests/parser-legacy/samples/attribute-dynamic-boolean/output.json b/packages/svelte/tests/parser-legacy/samples/attribute-dynamic-boolean/output.json index 5793afe896..13488a20d3 100644 --- a/packages/svelte/tests/parser-legacy/samples/attribute-dynamic-boolean/output.json +++ b/packages/svelte/tests/parser-legacy/samples/attribute-dynamic-boolean/output.json @@ -15,6 +15,18 @@ "start": 10, "end": 29, "name": "readonly", + "name_loc": { + "start": { + "line": 1, + "column": 10, + "character": 10 + }, + "end": { + "line": 1, + "column": 18, + "character": 18 + } + }, "value": [ { "type": "MustacheTag", diff --git a/packages/svelte/tests/parser-legacy/samples/attribute-dynamic/output.json b/packages/svelte/tests/parser-legacy/samples/attribute-dynamic/output.json index 9fd98c80ec..088a9d01ab 100644 --- a/packages/svelte/tests/parser-legacy/samples/attribute-dynamic/output.json +++ b/packages/svelte/tests/parser-legacy/samples/attribute-dynamic/output.json @@ -15,6 +15,18 @@ "start": 5, "end": 28, "name": "style", + "name_loc": { + "start": { + "line": 1, + "column": 5, + "character": 5 + }, + "end": { + "line": 1, + "column": 10, + "character": 10 + } + }, "value": [ { "start": 12, diff --git a/packages/svelte/tests/parser-legacy/samples/attribute-empty/output.json b/packages/svelte/tests/parser-legacy/samples/attribute-empty/output.json index d2a3dcd93b..028b11077d 100644 --- a/packages/svelte/tests/parser-legacy/samples/attribute-empty/output.json +++ b/packages/svelte/tests/parser-legacy/samples/attribute-empty/output.json @@ -15,6 +15,18 @@ "start": 5, "end": 9, "name": "a", + "name_loc": { + "start": { + "line": 1, + "column": 5, + "character": 5 + }, + "end": { + "line": 1, + "column": 6, + "character": 6 + } + }, "value": [ { "start": 8, @@ -30,6 +42,18 @@ "start": 10, "end": 16, "name": "b", + "name_loc": { + "start": { + "line": 1, + "column": 10, + "character": 10 + }, + "end": { + "line": 1, + "column": 11, + "character": 11 + } + }, "value": [ { "type": "MustacheTag", @@ -60,6 +84,18 @@ "start": 17, "end": 21, "name": "c", + "name_loc": { + "start": { + "line": 1, + "column": 17, + "character": 17 + }, + "end": { + "line": 1, + "column": 18, + "character": 18 + } + }, "value": [ { "start": 20, @@ -75,6 +111,18 @@ "start": 22, "end": 30, "name": "d", + "name_loc": { + "start": { + "line": 1, + "column": 22, + "character": 22 + }, + "end": { + "line": 1, + "column": 23, + "character": 23 + } + }, "value": [ { "type": "MustacheTag", diff --git a/packages/svelte/tests/parser-legacy/samples/attribute-escaped/output.json b/packages/svelte/tests/parser-legacy/samples/attribute-escaped/output.json index e2eb99f327..a23fcd55cd 100644 --- a/packages/svelte/tests/parser-legacy/samples/attribute-escaped/output.json +++ b/packages/svelte/tests/parser-legacy/samples/attribute-escaped/output.json @@ -15,6 +15,18 @@ "start": 5, "end": 76, "name": "data-foo", + "name_loc": { + "start": { + "line": 1, + "column": 5, + "character": 5 + }, + "end": { + "line": 1, + "column": 13, + "character": 13 + } + }, "value": [ { "start": 15, diff --git a/packages/svelte/tests/parser-legacy/samples/attribute-multiple/output.json b/packages/svelte/tests/parser-legacy/samples/attribute-multiple/output.json index 66b780e536..4e2bddf54e 100644 --- a/packages/svelte/tests/parser-legacy/samples/attribute-multiple/output.json +++ b/packages/svelte/tests/parser-legacy/samples/attribute-multiple/output.json @@ -15,6 +15,18 @@ "start": 5, "end": 11, "name": "id", + "name_loc": { + "start": { + "line": 1, + "column": 5, + "character": 5 + }, + "end": { + "line": 1, + "column": 7, + "character": 7 + } + }, "value": [ { "start": 9, @@ -30,6 +42,18 @@ "start": 12, "end": 21, "name": "class", + "name_loc": { + "start": { + "line": 1, + "column": 12, + "character": 12 + }, + "end": { + "line": 1, + "column": 17, + "character": 17 + } + }, "value": [ { "start": 19, diff --git a/packages/svelte/tests/parser-legacy/samples/attribute-shorthand/output.json b/packages/svelte/tests/parser-legacy/samples/attribute-shorthand/output.json index 2ae3acfdc7..61121e436e 100644 --- a/packages/svelte/tests/parser-legacy/samples/attribute-shorthand/output.json +++ b/packages/svelte/tests/parser-legacy/samples/attribute-shorthand/output.json @@ -15,16 +15,40 @@ "start": 5, "end": 9, "name": "id", + "name_loc": { + "start": { + "line": 1, + "column": 6, + "character": 6 + }, + "end": { + "line": 1, + "column": 8, + "character": 8 + } + }, "value": [ { "type": "AttributeShorthand", "start": 6, "end": 8, "expression": { + "type": "Identifier", + "name": "id", "start": 6, "end": 8, - "type": "Identifier", - "name": "id" + "loc": { + "start": { + "line": 1, + "column": 6, + "character": 6 + }, + "end": { + "line": 1, + "column": 8, + "character": 8 + } + } } } ] diff --git a/packages/svelte/tests/parser-legacy/samples/attribute-static-boolean/output.json b/packages/svelte/tests/parser-legacy/samples/attribute-static-boolean/output.json index 8cb93b75ec..1442a445d0 100644 --- a/packages/svelte/tests/parser-legacy/samples/attribute-static-boolean/output.json +++ b/packages/svelte/tests/parser-legacy/samples/attribute-static-boolean/output.json @@ -15,6 +15,18 @@ "start": 10, "end": 18, "name": "readonly", + "name_loc": { + "start": { + "line": 1, + "column": 10, + "character": 10 + }, + "end": { + "line": 1, + "column": 18, + "character": 18 + } + }, "value": true } ], diff --git a/packages/svelte/tests/parser-legacy/samples/attribute-static/output.json b/packages/svelte/tests/parser-legacy/samples/attribute-static/output.json index 3e19a4727e..dbc2a4b79f 100644 --- a/packages/svelte/tests/parser-legacy/samples/attribute-static/output.json +++ b/packages/svelte/tests/parser-legacy/samples/attribute-static/output.json @@ -15,6 +15,18 @@ "start": 5, "end": 16, "name": "class", + "name_loc": { + "start": { + "line": 1, + "column": 5, + "character": 5 + }, + "end": { + "line": 1, + "column": 10, + "character": 10 + } + }, "value": [ { "start": 12, diff --git a/packages/svelte/tests/parser-legacy/samples/attribute-style-directive-modifiers/output.json b/packages/svelte/tests/parser-legacy/samples/attribute-style-directive-modifiers/output.json index b7de71ff5a..da8a11e48e 100644 --- a/packages/svelte/tests/parser-legacy/samples/attribute-style-directive-modifiers/output.json +++ b/packages/svelte/tests/parser-legacy/samples/attribute-style-directive-modifiers/output.json @@ -15,7 +15,21 @@ "end": 36, "type": "StyleDirective", "name": "color", - "modifiers": ["important"], + "name_loc": { + "start": { + "line": 1, + "column": 5, + "character": 5 + }, + "end": { + "line": 1, + "column": 26, + "character": 26 + } + }, + "modifiers": [ + "important" + ], "value": [ { "type": "MustacheTag", diff --git a/packages/svelte/tests/parser-legacy/samples/attribute-style-directive-shorthand/output.json b/packages/svelte/tests/parser-legacy/samples/attribute-style-directive-shorthand/output.json index d7f53cb00c..b54a6e91d7 100644 --- a/packages/svelte/tests/parser-legacy/samples/attribute-style-directive-shorthand/output.json +++ b/packages/svelte/tests/parser-legacy/samples/attribute-style-directive-shorthand/output.json @@ -15,6 +15,18 @@ "end": 16, "type": "StyleDirective", "name": "color", + "name_loc": { + "start": { + "line": 1, + "column": 5, + "character": 5 + }, + "end": { + "line": 1, + "column": 16, + "character": 16 + } + }, "modifiers": [], "value": true } diff --git a/packages/svelte/tests/parser-legacy/samples/attribute-style-directive-string/output.json b/packages/svelte/tests/parser-legacy/samples/attribute-style-directive-string/output.json index 5acf7d797e..f349e61ef7 100644 --- a/packages/svelte/tests/parser-legacy/samples/attribute-style-directive-string/output.json +++ b/packages/svelte/tests/parser-legacy/samples/attribute-style-directive-string/output.json @@ -15,6 +15,18 @@ "end": 22, "type": "StyleDirective", "name": "color", + "name_loc": { + "start": { + "line": 1, + "column": 5, + "character": 5 + }, + "end": { + "line": 1, + "column": 16, + "character": 16 + } + }, "modifiers": [], "value": [ { @@ -47,6 +59,18 @@ "end": 52, "type": "StyleDirective", "name": "color", + "name_loc": { + "start": { + "line": 2, + "column": 5, + "character": 35 + }, + "end": { + "line": 2, + "column": 16, + "character": 46 + } + }, "modifiers": [], "value": [ { @@ -79,6 +103,18 @@ "end": 80, "type": "StyleDirective", "name": "color", + "name_loc": { + "start": { + "line": 3, + "column": 5, + "character": 65 + }, + "end": { + "line": 3, + "column": 16, + "character": 76 + } + }, "modifiers": [], "value": [ { @@ -111,6 +147,18 @@ "end": 120, "type": "StyleDirective", "name": "color", + "name_loc": { + "start": { + "line": 4, + "column": 5, + "character": 93 + }, + "end": { + "line": 4, + "column": 16, + "character": 104 + } + }, "modifiers": [], "value": [ { @@ -164,6 +212,18 @@ "end": 160, "type": "StyleDirective", "name": "color", + "name_loc": { + "start": { + "line": 5, + "column": 5, + "character": 133 + }, + "end": { + "line": 5, + "column": 16, + "character": 144 + } + }, "modifiers": [], "value": [ { @@ -217,6 +277,18 @@ "end": 198, "type": "StyleDirective", "name": "color", + "name_loc": { + "start": { + "line": 6, + "column": 5, + "character": 173 + }, + "end": { + "line": 6, + "column": 16, + "character": 184 + } + }, "modifiers": [], "value": [ { @@ -270,6 +342,18 @@ "end": 245, "type": "StyleDirective", "name": "color", + "name_loc": { + "start": { + "line": 7, + "column": 5, + "character": 211 + }, + "end": { + "line": 7, + "column": 16, + "character": 222 + } + }, "modifiers": [], "value": [ { diff --git a/packages/svelte/tests/parser-legacy/samples/attribute-style-directive/output.json b/packages/svelte/tests/parser-legacy/samples/attribute-style-directive/output.json index 2cce9fef95..34e899428c 100644 --- a/packages/svelte/tests/parser-legacy/samples/attribute-style-directive/output.json +++ b/packages/svelte/tests/parser-legacy/samples/attribute-style-directive/output.json @@ -15,6 +15,18 @@ "end": 26, "type": "StyleDirective", "name": "color", + "name_loc": { + "start": { + "line": 1, + "column": 5, + "character": 5 + }, + "end": { + "line": 1, + "column": 16, + "character": 16 + } + }, "modifiers": [], "value": [ { diff --git a/packages/svelte/tests/parser-legacy/samples/attribute-style/output.json b/packages/svelte/tests/parser-legacy/samples/attribute-style/output.json index 1d9a528d6d..272c48d6ed 100644 --- a/packages/svelte/tests/parser-legacy/samples/attribute-style/output.json +++ b/packages/svelte/tests/parser-legacy/samples/attribute-style/output.json @@ -15,6 +15,18 @@ "start": 5, "end": 24, "name": "style", + "name_loc": { + "start": { + "line": 1, + "column": 5, + "character": 5 + }, + "end": { + "line": 1, + "column": 10, + "character": 10 + } + }, "value": [ { "start": 12, diff --git a/packages/svelte/tests/parser-legacy/samples/attribute-unquoted/output.json b/packages/svelte/tests/parser-legacy/samples/attribute-unquoted/output.json index ab2912a2c0..fecbd9f290 100644 --- a/packages/svelte/tests/parser-legacy/samples/attribute-unquoted/output.json +++ b/packages/svelte/tests/parser-legacy/samples/attribute-unquoted/output.json @@ -15,6 +15,18 @@ "start": 5, "end": 14, "name": "class", + "name_loc": { + "start": { + "line": 1, + "column": 5, + "character": 5 + }, + "end": { + "line": 1, + "column": 10, + "character": 10 + } + }, "value": [ { "start": 11, @@ -46,6 +58,18 @@ "start": 25, "end": 31, "name": "href", + "name_loc": { + "start": { + "line": 2, + "column": 3, + "character": 25 + }, + "end": { + "line": 2, + "column": 7, + "character": 29 + } + }, "value": [ { "start": 30, @@ -85,6 +109,18 @@ "start": 44, "end": 53, "name": "href", + "name_loc": { + "start": { + "line": 3, + "column": 3, + "character": 44 + }, + "end": { + "line": 3, + "column": 7, + "character": 48 + } + }, "value": [ { "start": 49, diff --git a/packages/svelte/tests/parser-legacy/samples/attribute-with-whitespace/output.json b/packages/svelte/tests/parser-legacy/samples/attribute-with-whitespace/output.json index 2e45184be9..16cfcec195 100644 --- a/packages/svelte/tests/parser-legacy/samples/attribute-with-whitespace/output.json +++ b/packages/svelte/tests/parser-legacy/samples/attribute-with-whitespace/output.json @@ -15,6 +15,18 @@ "end": 23, "type": "EventHandler", "name": "click", + "name_loc": { + "start": { + "line": 1, + "column": 8, + "character": 8 + }, + "end": { + "line": 1, + "column": 16, + "character": 16 + } + }, "expression": { "type": "Identifier", "start": 19, diff --git a/packages/svelte/tests/parser-legacy/samples/await-catch/output.json b/packages/svelte/tests/parser-legacy/samples/await-catch/output.json index 5572d573f8..1ac7f1773d 100644 --- a/packages/svelte/tests/parser-legacy/samples/await-catch/output.json +++ b/packages/svelte/tests/parser-legacy/samples/await-catch/output.json @@ -29,6 +29,7 @@ "type": "Identifier", "name": "theError", "start": 47, + "end": 55, "loc": { "start": { "line": 3, @@ -40,8 +41,7 @@ "column": 16, "character": 55 } - }, - "end": 55 + } }, "pending": { "type": "PendingBlock", diff --git a/packages/svelte/tests/parser-legacy/samples/await-then-catch/output.json b/packages/svelte/tests/parser-legacy/samples/await-then-catch/output.json index b71365f39d..df76eab8d3 100644 --- a/packages/svelte/tests/parser-legacy/samples/await-then-catch/output.json +++ b/packages/svelte/tests/parser-legacy/samples/await-then-catch/output.json @@ -28,6 +28,7 @@ "type": "Identifier", "name": "theValue", "start": 46, + "end": 54, "loc": { "start": { "line": 3, @@ -39,13 +40,13 @@ "column": 15, "character": 54 } - }, - "end": 54 + } }, "error": { "type": "Identifier", "name": "theError", "start": 96, + "end": 104, "loc": { "start": { "line": 5, @@ -57,8 +58,7 @@ "column": 16, "character": 104 } - }, - "end": 104 + } }, "pending": { "type": "PendingBlock", 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 4289245705..711bd54525 100644 --- a/packages/svelte/tests/parser-legacy/samples/binding-shorthand/output.json +++ b/packages/svelte/tests/parser-legacy/samples/binding-shorthand/output.json @@ -22,6 +22,18 @@ "end": 46, "type": "Binding", "name": "foo", + "name_loc": { + "start": { + "line": 5, + "column": 8, + "character": 38 + }, + "end": { + "line": 5, + "column": 16, + "character": 46 + } + }, "expression": { "start": 43, "end": 46, @@ -51,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 5256ede7bb..b103e03683 100644 --- a/packages/svelte/tests/parser-legacy/samples/binding/output.json +++ b/packages/svelte/tests/parser-legacy/samples/binding/output.json @@ -22,6 +22,18 @@ "end": 55, "type": "Binding", "name": "value", + "name_loc": { + "start": { + "line": 5, + "column": 7, + "character": 38 + }, + "end": { + "line": 5, + "column": 17, + "character": 48 + } + }, "expression": { "type": "Identifier", "start": 50, @@ -61,7 +73,7 @@ }, "end": { "line": 3, - "column": 0 + "column": 9 } }, "body": [ diff --git a/packages/svelte/tests/parser-legacy/samples/comment-with-ignores/output.json b/packages/svelte/tests/parser-legacy/samples/comment-with-ignores/output.json index cf2dc9f752..2caf252de8 100644 --- a/packages/svelte/tests/parser-legacy/samples/comment-with-ignores/output.json +++ b/packages/svelte/tests/parser-legacy/samples/comment-with-ignores/output.json @@ -9,7 +9,10 @@ "start": 0, "end": 31, "data": " svelte-ignore foo, bar ", - "ignores": ["foo", "bar"] + "ignores": [ + "foo", + "bar" + ] } ] } diff --git a/packages/svelte/tests/parser-legacy/samples/dynamic-element-string/output.json b/packages/svelte/tests/parser-legacy/samples/dynamic-element-string/output.json index 9ba15d6044..4768501c46 100644 --- a/packages/svelte/tests/parser-legacy/samples/dynamic-element-string/output.json +++ b/packages/svelte/tests/parser-legacy/samples/dynamic-element-string/output.json @@ -156,6 +156,18 @@ "start": 263, "end": 274, "name": "class", + "name_loc": { + "start": { + "line": 7, + "column": 29, + "character": 263 + }, + "end": { + "line": 7, + "column": 34, + "character": 268 + } + }, "value": [ { "start": 270, diff --git a/packages/svelte/tests/parser-legacy/samples/dynamic-element-variable/output.json b/packages/svelte/tests/parser-legacy/samples/dynamic-element-variable/output.json index 291cdaa734..6d9a5c78ee 100644 --- a/packages/svelte/tests/parser-legacy/samples/dynamic-element-variable/output.json +++ b/packages/svelte/tests/parser-legacy/samples/dynamic-element-variable/output.json @@ -62,6 +62,18 @@ "start": 72, "end": 83, "name": "class", + "name_loc": { + "start": { + "line": 2, + "column": 27, + "character": 72 + }, + "end": { + "line": 2, + "column": 32, + "character": 77 + } + }, "value": [ { "start": 79, 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/each-block-else/output.json b/packages/svelte/tests/parser-legacy/samples/each-block-else/output.json index a6db309edb..c8458f96f0 100644 --- a/packages/svelte/tests/parser-legacy/samples/each-block-else/output.json +++ b/packages/svelte/tests/parser-legacy/samples/each-block-else/output.json @@ -44,6 +44,7 @@ "type": "Identifier", "name": "animal", "start": 18, + "end": 24, "loc": { "start": { "line": 1, @@ -55,8 +56,7 @@ "column": 24, "character": 24 } - }, - "end": 24 + } }, "expression": { "type": "Identifier", diff --git a/packages/svelte/tests/parser-legacy/samples/each-block-indexed/output.json b/packages/svelte/tests/parser-legacy/samples/each-block-indexed/output.json index bce7fd81a2..2ba41d56b8 100644 --- a/packages/svelte/tests/parser-legacy/samples/each-block-indexed/output.json +++ b/packages/svelte/tests/parser-legacy/samples/each-block-indexed/output.json @@ -72,6 +72,7 @@ "type": "Identifier", "name": "animal", "start": 18, + "end": 24, "loc": { "start": { "line": 1, @@ -83,8 +84,7 @@ "column": 24, "character": 24 } - }, - "end": 24 + } }, "expression": { "type": "Identifier", diff --git a/packages/svelte/tests/parser-legacy/samples/each-block-keyed/output.json b/packages/svelte/tests/parser-legacy/samples/each-block-keyed/output.json index 2f6206a6cb..569681506b 100644 --- a/packages/svelte/tests/parser-legacy/samples/each-block-keyed/output.json +++ b/packages/svelte/tests/parser-legacy/samples/each-block-keyed/output.json @@ -44,6 +44,7 @@ "type": "Identifier", "name": "todo", "start": 16, + "end": 20, "loc": { "start": { "line": 1, @@ -55,8 +56,7 @@ "column": 20, "character": 20 } - }, - "end": 20 + } }, "expression": { "type": "Identifier", diff --git a/packages/svelte/tests/parser-legacy/samples/each-block/output.json b/packages/svelte/tests/parser-legacy/samples/each-block/output.json index f26f557958..c763dc6b85 100644 --- a/packages/svelte/tests/parser-legacy/samples/each-block/output.json +++ b/packages/svelte/tests/parser-legacy/samples/each-block/output.json @@ -44,6 +44,7 @@ "type": "Identifier", "name": "animal", "start": 18, + "end": 24, "loc": { "start": { "line": 1, @@ -55,8 +56,7 @@ "column": 24, "character": 24 } - }, - "end": 24 + } }, "expression": { "type": "Identifier", diff --git a/packages/svelte/tests/parser-legacy/samples/element-with-attribute-empty-string/output.json b/packages/svelte/tests/parser-legacy/samples/element-with-attribute-empty-string/output.json index 7773256d44..2d6a4b752c 100644 --- a/packages/svelte/tests/parser-legacy/samples/element-with-attribute-empty-string/output.json +++ b/packages/svelte/tests/parser-legacy/samples/element-with-attribute-empty-string/output.json @@ -15,6 +15,18 @@ "start": 6, "end": 13, "name": "attr", + "name_loc": { + "start": { + "line": 1, + "column": 6, + "character": 6 + }, + "end": { + "line": 1, + "column": 10, + "character": 10 + } + }, "value": [ { "start": 12, @@ -46,6 +58,18 @@ "start": 28, "end": 35, "name": "attr", + "name_loc": { + "start": { + "line": 2, + "column": 6, + "character": 28 + }, + "end": { + "line": 2, + "column": 10, + "character": 32 + } + }, "value": [ { "start": 34, diff --git a/packages/svelte/tests/parser-legacy/samples/element-with-attribute/output.json b/packages/svelte/tests/parser-legacy/samples/element-with-attribute/output.json index 9477886bb2..9fdb601b67 100644 --- a/packages/svelte/tests/parser-legacy/samples/element-with-attribute/output.json +++ b/packages/svelte/tests/parser-legacy/samples/element-with-attribute/output.json @@ -15,6 +15,18 @@ "start": 6, "end": 16, "name": "attr", + "name_loc": { + "start": { + "line": 1, + "column": 6, + "character": 6 + }, + "end": { + "line": 1, + "column": 10, + "character": 10 + } + }, "value": [ { "start": 12, @@ -46,6 +58,18 @@ "start": 31, "end": 41, "name": "attr", + "name_loc": { + "start": { + "line": 2, + "column": 6, + "character": 31 + }, + "end": { + "line": 2, + "column": 10, + "character": 35 + } + }, "value": [ { "start": 37, diff --git a/packages/svelte/tests/parser-legacy/samples/elements/output.json b/packages/svelte/tests/parser-legacy/samples/elements/output.json index 6b51383d93..afc49c56c3 100644 --- a/packages/svelte/tests/parser-legacy/samples/elements/output.json +++ b/packages/svelte/tests/parser-legacy/samples/elements/output.json @@ -15,6 +15,18 @@ "start": 10, "end": 14, "name": "html", + "name_loc": { + "start": { + "line": 1, + "column": 10, + "character": 10 + }, + "end": { + "line": 1, + "column": 14, + "character": 14 + } + }, "value": true } ], diff --git a/packages/svelte/tests/parser-legacy/samples/event-handler/output.json b/packages/svelte/tests/parser-legacy/samples/event-handler/output.json index 11ee562297..8b054a4289 100644 --- a/packages/svelte/tests/parser-legacy/samples/event-handler/output.json +++ b/packages/svelte/tests/parser-legacy/samples/event-handler/output.json @@ -15,6 +15,18 @@ "end": 45, "type": "EventHandler", "name": "click", + "name_loc": { + "start": { + "line": 1, + "column": 8, + "character": 8 + }, + "end": { + "line": 1, + "column": 16, + "character": 16 + } + }, "expression": { "type": "ArrowFunctionExpression", "start": 19, 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 37fb499e7b..76605f5192 100644 --- a/packages/svelte/tests/parser-legacy/samples/generic-snippets/output.json +++ b/packages/svelte/tests/parser-legacy/samples/generic-snippets/output.json @@ -17,9 +17,21 @@ "end": 92, "expression": { "type": "Identifier", + "name": "generic", "start": 40, "end": 47, - "name": "generic" + "loc": { + "start": { + "line": 4, + "column": 10, + "character": 40 + }, + "end": { + "line": 4, + "column": 17, + "character": 47 + } + } }, "parameters": [ { @@ -123,9 +135,21 @@ "end": 192, "expression": { "type": "Identifier", + "name": "complex_generic", "start": 104, "end": 119, - "name": "complex_generic" + "loc": { + "start": { + "line": 8, + "column": 10, + "character": 104 + }, + "end": { + "line": 8, + "column": 25, + "character": 119 + } + } }, "parameters": [ { @@ -234,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 42229b741f..3775c48228 100644 --- a/packages/svelte/tests/parser-legacy/samples/javascript-comments/output.json +++ b/packages/svelte/tests/parser-legacy/samples/javascript-comments/output.json @@ -22,6 +22,18 @@ "end": 692, "type": "EventHandler", "name": "click", + "name_loc": { + "start": { + "line": 34, + "column": 1, + "character": 574 + }, + "end": { + "line": 34, + "column": 9, + "character": 582 + } + }, "expression": { "type": "ArrowFunctionExpression", "start": 596, @@ -286,7 +298,7 @@ }, "end": { "line": 31, - "column": 0 + "column": 9 } }, "body": [ diff --git a/packages/svelte/tests/parser-legacy/samples/loose-invalid-block/output.json b/packages/svelte/tests/parser-legacy/samples/loose-invalid-block/output.json index 480fcf2edc..c4d0dd4aa4 100644 --- a/packages/svelte/tests/parser-legacy/samples/loose-invalid-block/output.json +++ b/packages/svelte/tests/parser-legacy/samples/loose-invalid-block/output.json @@ -75,9 +75,21 @@ "end": 75, "expression": { "type": "Identifier", + "name": "", "start": 63, "end": 63, - "name": "" + "loc": { + "start": { + "line": 9, + "column": 10, + "character": 63 + }, + "end": { + "line": 9, + "column": 10, + "character": 63 + } + } }, "parameters": [], "children": [] @@ -95,9 +107,21 @@ "end": 102, "expression": { "type": "Identifier", + "name": "foo", "start": 87, "end": 90, - "name": "foo" + "loc": { + "start": { + "line": 12, + "column": 10, + "character": 87 + }, + "end": { + "line": 12, + "column": 13, + "character": 90 + } + } }, "parameters": [], "children": [] diff --git a/packages/svelte/tests/parser-legacy/samples/loose-invalid-expression/output.json b/packages/svelte/tests/parser-legacy/samples/loose-invalid-expression/output.json index 0564d6d295..136c45fc6e 100644 --- a/packages/svelte/tests/parser-legacy/samples/loose-invalid-expression/output.json +++ b/packages/svelte/tests/parser-legacy/samples/loose-invalid-expression/output.json @@ -15,16 +15,40 @@ "start": 5, "end": 7, "name": "", + "name_loc": { + "start": { + "line": 1, + "column": 6, + "character": 6 + }, + "end": { + "line": 1, + "column": 6, + "character": 6 + } + }, "value": [ { "type": "AttributeShorthand", "start": 6, "end": 6, "expression": { + "type": "Identifier", + "name": "", "start": 6, "end": 6, - "type": "Identifier", - "name": "" + "loc": { + "start": { + "line": 1, + "column": 6, + "character": 6 + }, + "end": { + "line": 1, + "column": 6, + "character": 6 + } + } } } ] @@ -50,6 +74,18 @@ "start": 20, "end": 26, "name": "foo", + "name_loc": { + "start": { + "line": 2, + "column": 5, + "character": 20 + }, + "end": { + "line": 2, + "column": 8, + "character": 23 + } + }, "value": [ { "type": "MustacheTag", @@ -85,6 +121,18 @@ "start": 40, "end": 48, "name": "foo", + "name_loc": { + "start": { + "line": 4, + "column": 5, + "character": 40 + }, + "end": { + "line": 4, + "column": 8, + "character": 43 + } + }, "value": [ { "type": "MustacheTag", @@ -120,6 +168,18 @@ "start": 61, "end": 73, "name": "foo", + "name_loc": { + "start": { + "line": 5, + "column": 5, + "character": 61 + }, + "end": { + "line": 5, + "column": 8, + "character": 64 + } + }, "value": [ { "type": "MustacheTag", @@ -155,6 +215,18 @@ "start": 92, "end": 110, "name": "onclick", + "name_loc": { + "start": { + "line": 6, + "column": 11, + "character": 92 + }, + "end": { + "line": 6, + "column": 18, + "character": 99 + } + }, "value": [ { "type": "MustacheTag", @@ -190,6 +262,18 @@ "end": 137, "type": "Binding", "name": "value", + "name_loc": { + "start": { + "line": 8, + "column": 7, + "character": 122 + }, + "end": { + "line": 8, + "column": 17, + "character": 132 + } + }, "expression": { "type": "Identifier", "start": 134, @@ -272,6 +356,7 @@ "type": "Identifier", "name": "item", "start": 197, + "end": 201, "loc": { "start": { "line": 15, @@ -283,8 +368,7 @@ "column": 20, "character": 201 } - }, - "end": 201 + } }, "expression": { "type": "Identifier", @@ -325,6 +409,7 @@ "type": "Identifier", "name": "item", "start": 234, + "end": 238, "loc": { "start": { "line": 17, @@ -336,8 +421,7 @@ "column": 19, "character": 238 } - }, - "end": 238 + } }, "expression": { "type": "Identifier", @@ -408,6 +492,7 @@ "type": "Identifier", "name": "y", "start": 285, + "end": 286, "loc": { "start": { "line": 21, @@ -419,8 +504,7 @@ "column": 17, "character": 286 } - }, - "end": 286 + } }, "error": null, "pending": { @@ -467,6 +551,7 @@ "type": "Identifier", "name": "y", "start": 314, + "end": 315, "loc": { "start": { "line": 23, @@ -478,8 +563,7 @@ "column": 18, "character": 315 } - }, - "end": 315 + } }, "pending": { "type": "PendingBlock", diff --git a/packages/svelte/tests/parser-legacy/samples/loose-unclosed-block/output.json b/packages/svelte/tests/parser-legacy/samples/loose-unclosed-block/output.json index e6e909f0d5..37a46a48da 100644 --- a/packages/svelte/tests/parser-legacy/samples/loose-unclosed-block/output.json +++ b/packages/svelte/tests/parser-legacy/samples/loose-unclosed-block/output.json @@ -240,6 +240,7 @@ "type": "Identifier", "name": "y", "start": 138, + "end": 139, "loc": { "start": { "line": 19, @@ -251,8 +252,7 @@ "column": 13, "character": 139 } - }, - "end": 139 + } }, "expression": { "type": "Identifier", diff --git a/packages/svelte/tests/parser-legacy/samples/loose-unclosed-open-tag/output.json b/packages/svelte/tests/parser-legacy/samples/loose-unclosed-open-tag/output.json index 1792d6b8e6..9d5d0e7658 100644 --- a/packages/svelte/tests/parser-legacy/samples/loose-unclosed-open-tag/output.json +++ b/packages/svelte/tests/parser-legacy/samples/loose-unclosed-open-tag/output.json @@ -29,6 +29,18 @@ "start": 13, "end": 22, "name": "foo", + "name_loc": { + "start": { + "line": 2, + "column": 7, + "character": 13 + }, + "end": { + "line": 2, + "column": 10, + "character": 16 + } + }, "value": [ { "type": "MustacheTag", @@ -90,6 +102,18 @@ "start": 44, "end": 53, "name": "foo", + "name_loc": { + "start": { + "line": 6, + "column": 7, + "character": 44 + }, + "end": { + "line": 6, + "column": 10, + "character": 47 + } + }, "value": [ { "type": "MustacheTag", @@ -158,6 +182,18 @@ "start": 79, "end": 88, "name": "foo", + "name_loc": { + "start": { + "line": 10, + "column": 7, + "character": 79 + }, + "end": { + "line": 10, + "column": 10, + "character": 82 + } + }, "value": [ { "type": "MustacheTag", @@ -226,6 +262,18 @@ "start": 113, "end": 122, "name": "foo", + "name_loc": { + "start": { + "line": 14, + "column": 7, + "character": 113 + }, + "end": { + "line": 14, + "column": 10, + "character": 116 + } + }, "value": [ { "type": "MustacheTag", @@ -317,6 +365,18 @@ "start": 161, "end": 170, "name": "foo", + "name_loc": { + "start": { + "line": 20, + "column": 5, + "character": 161 + }, + "end": { + "line": 20, + "column": 8, + "character": 164 + } + }, "value": [ { "type": "MustacheTag", diff --git a/packages/svelte/tests/parser-legacy/samples/loose-unclosed-tag/output.json b/packages/svelte/tests/parser-legacy/samples/loose-unclosed-tag/output.json index 2205a00e20..1ff3342de0 100644 --- a/packages/svelte/tests/parser-legacy/samples/loose-unclosed-tag/output.json +++ b/packages/svelte/tests/parser-legacy/samples/loose-unclosed-tag/output.json @@ -68,6 +68,18 @@ "start": 35, "end": 44, "name": "foo", + "name_loc": { + "start": { + "line": 6, + "column": 7, + "character": 35 + }, + "end": { + "line": 6, + "column": 10, + "character": 38 + } + }, "value": [ { "type": "MustacheTag", @@ -275,6 +287,18 @@ "start": 159, "end": 168, "name": "foo", + "name_loc": { + "start": { + "line": 26, + "column": 7, + "character": 159 + }, + "end": { + "line": 26, + "column": 10, + "character": 162 + } + }, "value": [ { "type": "MustacheTag", diff --git a/packages/svelte/tests/parser-legacy/samples/no-error-if-before-closing/output.json b/packages/svelte/tests/parser-legacy/samples/no-error-if-before-closing/output.json index c60efd4fba..de12cffd86 100644 --- a/packages/svelte/tests/parser-legacy/samples/no-error-if-before-closing/output.json +++ b/packages/svelte/tests/parser-legacy/samples/no-error-if-before-closing/output.json @@ -119,6 +119,7 @@ "type": "Identifier", "name": "f", "start": 97, + "end": 98, "loc": { "start": { "line": 13, @@ -130,8 +131,7 @@ "column": 8, "character": 98 } - }, - "end": 98 + } }, "error": null, "pending": { @@ -219,6 +219,7 @@ "type": "Identifier", "name": "f", "start": 137, + "end": 138, "loc": { "start": { "line": 18, @@ -230,8 +231,7 @@ "column": 8, "character": 138 } - }, - "end": 138 + } }, "error": null, "pending": { diff --git a/packages/svelte/tests/parser-legacy/samples/refs/output.json b/packages/svelte/tests/parser-legacy/samples/refs/output.json index 7829a2787f..1e8efe78f8 100644 --- a/packages/svelte/tests/parser-legacy/samples/refs/output.json +++ b/packages/svelte/tests/parser-legacy/samples/refs/output.json @@ -22,6 +22,18 @@ "end": 53, "type": "Binding", "name": "this", + "name_loc": { + "start": { + "line": 5, + "column": 8, + "character": 38 + }, + "end": { + "line": 5, + "column": 17, + "character": 47 + } + }, "expression": { "type": "Identifier", "start": 49, @@ -61,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/self-reference/output.json b/packages/svelte/tests/parser-legacy/samples/self-reference/output.json index 34310fcce4..1ca684931b 100644 --- a/packages/svelte/tests/parser-legacy/samples/self-reference/output.json +++ b/packages/svelte/tests/parser-legacy/samples/self-reference/output.json @@ -69,6 +69,18 @@ "start": 30, "end": 49, "name": "depth", + "name_loc": { + "start": { + "line": 2, + "column": 14, + "character": 30 + }, + "end": { + "line": 2, + "column": 19, + "character": 35 + } + }, "value": [ { "type": "MustacheTag", diff --git a/packages/svelte/tests/parser-legacy/samples/slotted-element/output.json b/packages/svelte/tests/parser-legacy/samples/slotted-element/output.json index 90ded68103..caa0b86c45 100644 --- a/packages/svelte/tests/parser-legacy/samples/slotted-element/output.json +++ b/packages/svelte/tests/parser-legacy/samples/slotted-element/output.json @@ -22,6 +22,18 @@ "start": 16, "end": 26, "name": "slot", + "name_loc": { + "start": { + "line": 1, + "column": 16, + "character": 16 + }, + "end": { + "line": 1, + "column": 20, + "character": 20 + } + }, "value": [ { "start": 22, diff --git a/packages/svelte/tests/parser-legacy/samples/transition-intro-no-params/output.json b/packages/svelte/tests/parser-legacy/samples/transition-intro-no-params/output.json index 18860d615b..9f0d007cc8 100644 --- a/packages/svelte/tests/parser-legacy/samples/transition-intro-no-params/output.json +++ b/packages/svelte/tests/parser-legacy/samples/transition-intro-no-params/output.json @@ -15,6 +15,18 @@ "end": 12, "type": "Transition", "name": "fade", + "name_loc": { + "start": { + "line": 1, + "column": 5, + "character": 5 + }, + "end": { + "line": 1, + "column": 12, + "character": 12 + } + }, "expression": null, "modifiers": [], "intro": true, diff --git a/packages/svelte/tests/parser-legacy/samples/transition-intro/output.json b/packages/svelte/tests/parser-legacy/samples/transition-intro/output.json index 973cfb7d33..61b4afbc13 100644 --- a/packages/svelte/tests/parser-legacy/samples/transition-intro/output.json +++ b/packages/svelte/tests/parser-legacy/samples/transition-intro/output.json @@ -15,6 +15,18 @@ "end": 30, "type": "Transition", "name": "style", + "name_loc": { + "start": { + "line": 1, + "column": 5, + "character": 5 + }, + "end": { + "line": 1, + "column": 13, + "character": 13 + } + }, "expression": { "type": "ObjectExpression", "start": 16, diff --git a/packages/svelte/tests/parser-legacy/samples/unusual-identifier/output.json b/packages/svelte/tests/parser-legacy/samples/unusual-identifier/output.json index 9081b7cb92..da5555a777 100644 --- a/packages/svelte/tests/parser-legacy/samples/unusual-identifier/output.json +++ b/packages/svelte/tests/parser-legacy/samples/unusual-identifier/output.json @@ -44,6 +44,7 @@ "type": "Identifier", "name": "𐊧", "start": 17, + "end": 19, "loc": { "start": { "line": 1, @@ -55,8 +56,7 @@ "column": 19, "character": 19 } - }, - "end": 19 + } }, "expression": { "type": "Identifier", 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-legacy/test.ts b/packages/svelte/tests/parser-legacy/test.ts index 987253d8d0..2e64389404 100644 --- a/packages/svelte/tests/parser-legacy/test.ts +++ b/packages/svelte/tests/parser-legacy/test.ts @@ -18,7 +18,7 @@ const { test, run } = suite(async (config, cwd) => { // run `UPDATE_SNAPSHOTS=true pnpm test parser` to update parser tests if (process.env.UPDATE_SNAPSHOTS) { - fs.writeFileSync(`${cwd}/output.json`, JSON.stringify(actual, null, '\t')); + fs.writeFileSync(`${cwd}/output.json`, JSON.stringify(actual, null, '\t') + '\n'); } else { fs.writeFileSync(`${cwd}/_actual.json`, JSON.stringify(actual, null, '\t')); diff --git a/packages/svelte/tests/parser-modern/samples/attachments/output.json b/packages/svelte/tests/parser-modern/samples/attachments/output.json index 42e9880fcc..d37fec7beb 100644 --- a/packages/svelte/tests/parser-modern/samples/attachments/output.json +++ b/packages/svelte/tests/parser-modern/samples/attachments/output.json @@ -12,6 +12,18 @@ "start": 0, "end": 57, "name": "div", + "name_loc": { + "start": { + "line": 1, + "column": 1, + "character": 1 + }, + "end": { + "line": 1, + "column": 4, + "character": 4 + } + }, "attributes": [ { "type": "AttachTag", 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 dba258a6b1..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 @@ -1,7 +1,7 @@ { "css": null, "js": [], - "start": 37, + "start": 0, "end": 117, "type": "Root", "fragment": { @@ -19,12 +19,36 @@ "start": 37, "end": 117, "name": "input", + "name_loc": { + "start": { + "line": 5, + "column": 1, + "character": 38 + }, + "end": { + "line": 5, + "column": 6, + "character": 43 + } + }, "attributes": [ { "start": 44, "end": 114, "type": "BindDirective", "name": "value", + "name_loc": { + "start": { + "line": 5, + "column": 7, + "character": 44 + }, + "end": { + "line": 5, + "column": 17, + "character": 54 + } + }, "expression": { "type": "SequenceExpression", "start": 68, @@ -248,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 1aca0ce036..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 @@ -2,7 +2,7 @@ "css": null, "js": [], "start": 0, - "end": 27, + "end": 76, "type": "Root", "fragment": { "type": "Fragment", @@ -34,12 +34,12 @@ "end": 67, "loc": { "start": { - "line": 1, + "line": 2, "column": 0 }, "end": { "line": 4, - "column": 0 + "column": 9 } }, "body": [ @@ -138,6 +138,18 @@ "start": 36, "end": 45, "name": "lang", + "name_loc": { + "start": { + "line": 2, + "column": 8, + "character": 36 + }, + "end": { + "line": 2, + "column": 12, + "character": 40 + } + }, "value": [ { "start": 42, diff --git a/packages/svelte/tests/parser-modern/samples/css-nth-syntax/output.json b/packages/svelte/tests/parser-modern/samples/css-nth-syntax/output.json index a126acb4c3..3ce5f33d09 100644 --- a/packages/svelte/tests/parser-modern/samples/css-nth-syntax/output.json +++ b/packages/svelte/tests/parser-modern/samples/css-nth-syntax/output.json @@ -1079,7 +1079,7 @@ } }, "js": [], - "start": 808, + "start": 0, "end": 820, "type": "Root", "fragment": { @@ -1097,6 +1097,18 @@ "start": 808, "end": 820, "name": "h1", + "name_loc": { + "start": { + "line": 46, + "column": 1, + "character": 809 + }, + "end": { + "line": 46, + "column": 3, + "character": 811 + } + }, "attributes": [], "fragment": { "type": "Fragment", diff --git a/packages/svelte/tests/parser-modern/samples/css-pseudo-classes/output.json b/packages/svelte/tests/parser-modern/samples/css-pseudo-classes/output.json index e410cf2a80..d052affe46 100644 --- a/packages/svelte/tests/parser-modern/samples/css-pseudo-classes/output.json +++ b/packages/svelte/tests/parser-modern/samples/css-pseudo-classes/output.json @@ -398,8 +398,8 @@ } }, "js": [], - "start": null, - "end": null, + "start": 0, + "end": 386, "type": "Root", "fragment": { "type": "Fragment", diff --git a/packages/svelte/tests/parser-modern/samples/each-block-object-pattern-special-characters/output.json b/packages/svelte/tests/parser-modern/samples/each-block-object-pattern-special-characters/output.json index b962db3226..7fd5deb3ea 100644 --- a/packages/svelte/tests/parser-modern/samples/each-block-object-pattern-special-characters/output.json +++ b/packages/svelte/tests/parser-modern/samples/each-block-object-pattern-special-characters/output.json @@ -79,7 +79,6 @@ }, "name": "y" }, - "kind": "init", "value": { "type": "AssignmentPattern", "start": 14, @@ -127,7 +126,8 @@ "value": "z", "raw": "'z'" } - } + }, + "kind": "init" } ] } @@ -211,7 +211,6 @@ }, "name": "y" }, - "kind": "init", "value": { "type": "AssignmentPattern", "start": 47, @@ -259,7 +258,8 @@ "value": "{", "raw": "'{'" } - } + }, + "kind": "init" } ] } @@ -343,7 +343,6 @@ }, "name": "y" }, - "kind": "init", "value": { "type": "AssignmentPattern", "start": 80, @@ -391,7 +390,8 @@ "value": "]", "raw": "']'" } - } + }, + "kind": "init" } ] } @@ -475,7 +475,6 @@ }, "name": "y" }, - "kind": "init", "value": { "type": "AssignmentPattern", "start": 113, @@ -603,7 +602,8 @@ } ] } - } + }, + "kind": "init" } ] } @@ -687,7 +687,6 @@ }, "name": "y" }, - "kind": "init", "value": { "type": "AssignmentPattern", "start": 151, @@ -815,7 +814,8 @@ } ] } - } + }, + "kind": "init" } ] } diff --git a/packages/svelte/tests/parser-modern/samples/each-block-object-pattern/output.json b/packages/svelte/tests/parser-modern/samples/each-block-object-pattern/output.json index 144016417b..7e2fe09ea2 100644 --- a/packages/svelte/tests/parser-modern/samples/each-block-object-pattern/output.json +++ b/packages/svelte/tests/parser-modern/samples/each-block-object-pattern/output.json @@ -42,6 +42,18 @@ "start": 41, "end": 86, "name": "p", + "name_loc": { + "start": { + "line": 2, + "column": 2, + "character": 42 + }, + "end": { + "line": 2, + "column": 3, + "character": 43 + } + }, "attributes": [], "fragment": { "type": "Fragment", @@ -204,7 +216,6 @@ }, "name": "name" }, - "kind": "init", "value": { "type": "Identifier", "start": 19, @@ -220,7 +231,8 @@ } }, "name": "name" - } + }, + "kind": "init" }, { "type": "Property", @@ -255,7 +267,6 @@ }, "name": "cool" }, - "kind": "init", "value": { "type": "AssignmentPattern", "start": 25, @@ -303,7 +314,8 @@ "value": true, "raw": "true" } - } + }, + "kind": "init" } ] } @@ -350,6 +362,18 @@ "start": 155, "end": 200, "name": "p", + "name_loc": { + "start": { + "line": 6, + "column": 2, + "character": 156 + }, + "end": { + "line": 6, + "column": 3, + "character": 157 + } + }, "attributes": [], "fragment": { "type": "Fragment", @@ -512,7 +536,6 @@ }, "name": "name" }, - "kind": "init", "value": { "type": "AssignmentPattern", "start": 115, @@ -619,7 +642,8 @@ } ] } - } + }, + "kind": "init" }, { "type": "Property", @@ -654,7 +678,6 @@ }, "name": "cool" }, - "kind": "init", "value": { "type": "AssignmentPattern", "start": 139, @@ -702,7 +725,8 @@ "value": true, "raw": "true" } - } + }, + "kind": "init" } ] } @@ -749,6 +773,18 @@ "start": 291, "end": 336, "name": "p", + "name_loc": { + "start": { + "line": 10, + "column": 2, + "character": 292 + }, + "end": { + "line": 10, + "column": 3, + "character": 293 + } + }, "attributes": [], "fragment": { "type": "Fragment", @@ -911,7 +947,6 @@ }, "name": "name" }, - "kind": "init", "value": { "type": "AssignmentPattern", "start": 229, @@ -1087,7 +1122,8 @@ "arguments": [], "optional": false } - } + }, + "kind": "init" }, { "type": "Property", @@ -1122,7 +1158,6 @@ }, "name": "cool" }, - "kind": "init", "value": { "type": "AssignmentPattern", "start": 275, @@ -1170,7 +1205,8 @@ "value": true, "raw": "true" } - } + }, + "kind": "init" } ] } 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 b66ee7288f..f29eb6fc72 100644 --- a/packages/svelte/tests/parser-modern/samples/generic-snippets/output.json +++ b/packages/svelte/tests/parser-modern/samples/generic-snippets/output.json @@ -1,7 +1,7 @@ { "css": null, "js": [], - "start": 30, + "start": 0, "end": 192, "type": "Root", "fragment": { @@ -20,9 +20,21 @@ "end": 92, "expression": { "type": "Identifier", + "name": "generic", "start": 40, "end": 47, - "name": "generic" + "loc": { + "start": { + "line": 4, + "column": 10, + "character": 40 + }, + "end": { + "line": 4, + "column": 17, + "character": 47 + } + } }, "typeParams": "T extends string", "parameters": [ @@ -143,9 +155,21 @@ "end": 192, "expression": { "type": "Identifier", + "name": "complex_generic", "start": 104, "end": 119, - "name": "complex_generic" + "loc": { + "start": { + "line": 8, + "column": 10, + "character": 104 + }, + "end": { + "line": 8, + "column": 25, + "character": 119 + } + } }, "typeParams": "T extends { bracket: \"<\" } | \"<\" | Set<\"<>\">", "parameters": [ @@ -272,7 +296,7 @@ }, "end": { "line": 2, - "column": 0 + "column": 9 } }, "body": [], @@ -284,6 +308,18 @@ "start": 8, "end": 17, "name": "lang", + "name_loc": { + "start": { + "line": 1, + "column": 8, + "character": 8 + }, + "end": { + "line": 1, + "column": 12, + "character": 12 + } + }, "value": [ { "start": 14, diff --git a/packages/svelte/tests/parser-modern/samples/if-block-else/output.json b/packages/svelte/tests/parser-modern/samples/if-block-else/output.json index cf47e3e0bb..13b56db9de 100644 --- a/packages/svelte/tests/parser-modern/samples/if-block-else/output.json +++ b/packages/svelte/tests/parser-modern/samples/if-block-else/output.json @@ -43,6 +43,18 @@ "start": 11, "end": 21, "name": "p", + "name_loc": { + "start": { + "line": 2, + "column": 2, + "character": 12 + }, + "end": { + "line": 2, + "column": 3, + "character": 13 + } + }, "attributes": [], "fragment": { "type": "Fragment", @@ -81,6 +93,18 @@ "start": 31, "end": 45, "name": "p", + "name_loc": { + "start": { + "line": 4, + "column": 2, + "character": 32 + }, + "end": { + "line": 4, + "column": 3, + "character": 33 + } + }, "attributes": [], "fragment": { "type": "Fragment", diff --git a/packages/svelte/tests/parser-modern/samples/if-block-elseif/output.json b/packages/svelte/tests/parser-modern/samples/if-block-elseif/output.json index b10d50c939..fb00488f31 100644 --- a/packages/svelte/tests/parser-modern/samples/if-block-elseif/output.json +++ b/packages/svelte/tests/parser-modern/samples/if-block-elseif/output.json @@ -76,6 +76,18 @@ "start": 14, "end": 41, "name": "p", + "name_loc": { + "start": { + "line": 2, + "column": 2, + "character": 15 + }, + "end": { + "line": 2, + "column": 3, + "character": 16 + } + }, "attributes": [], "fragment": { "type": "Fragment", @@ -171,6 +183,18 @@ "start": 60, "end": 83, "name": "p", + "name_loc": { + "start": { + "line": 4, + "column": 2, + "character": 61 + }, + "end": { + "line": 4, + "column": 3, + "character": 62 + } + }, "attributes": [], "fragment": { "type": "Fragment", diff --git a/packages/svelte/tests/parser-modern/samples/loose-invalid-block/output.json b/packages/svelte/tests/parser-modern/samples/loose-invalid-block/output.json index 46aad16b21..69fc7a0c81 100644 --- a/packages/svelte/tests/parser-modern/samples/loose-invalid-block/output.json +++ b/packages/svelte/tests/parser-modern/samples/loose-invalid-block/output.json @@ -116,9 +116,21 @@ "end": 75, "expression": { "type": "Identifier", + "name": "", "start": 63, "end": 63, - "name": "" + "loc": { + "start": { + "line": 9, + "column": 10, + "character": 63 + }, + "end": { + "line": 9, + "column": 10, + "character": 63 + } + } }, "parameters": [], "body": { @@ -147,9 +159,21 @@ "end": 102, "expression": { "type": "Identifier", + "name": "foo", "start": 87, "end": 90, - "name": "foo" + "loc": { + "start": { + "line": 12, + "column": 10, + "character": 87 + }, + "end": { + "line": 12, + "column": 13, + "character": 90 + } + } }, "parameters": [], "body": { diff --git a/packages/svelte/tests/parser-modern/samples/loose-invalid-expression/output.json b/packages/svelte/tests/parser-modern/samples/loose-invalid-expression/output.json index 56fa4286dd..593aebe279 100644 --- a/packages/svelte/tests/parser-modern/samples/loose-invalid-expression/output.json +++ b/packages/svelte/tests/parser-modern/samples/loose-invalid-expression/output.json @@ -12,21 +12,57 @@ "start": 0, "end": 14, "name": "div", + "name_loc": { + "start": { + "line": 1, + "column": 1, + "character": 1 + }, + "end": { + "line": 1, + "column": 4, + "character": 4 + } + }, "attributes": [ { "type": "Attribute", "start": 5, "end": 7, "name": "", + "name_loc": { + "start": { + "line": 1, + "column": 6, + "character": 6 + }, + "end": { + "line": 1, + "column": 6, + "character": 6 + } + }, "value": { "type": "ExpressionTag", "start": 6, "end": 6, "expression": { + "type": "Identifier", + "name": "", "start": 6, "end": 6, - "type": "Identifier", - "name": "" + "loc": { + "start": { + "line": 1, + "column": 6, + "character": 6 + }, + "end": { + "line": 1, + "column": 6, + "character": 6 + } + } } } } @@ -48,12 +84,36 @@ "start": 15, "end": 33, "name": "div", + "name_loc": { + "start": { + "line": 2, + "column": 1, + "character": 16 + }, + "end": { + "line": 2, + "column": 4, + "character": 19 + } + }, "attributes": [ { "type": "Attribute", "start": 20, "end": 26, "name": "foo", + "name_loc": { + "start": { + "line": 2, + "column": 5, + "character": 20 + }, + "end": { + "line": 2, + "column": 8, + "character": 23 + } + }, "value": { "type": "ExpressionTag", "start": 24, @@ -84,12 +144,36 @@ "start": 35, "end": 55, "name": "div", + "name_loc": { + "start": { + "line": 4, + "column": 1, + "character": 36 + }, + "end": { + "line": 4, + "column": 4, + "character": 39 + } + }, "attributes": [ { "type": "Attribute", "start": 40, "end": 48, "name": "foo", + "name_loc": { + "start": { + "line": 4, + "column": 5, + "character": 40 + }, + "end": { + "line": 4, + "column": 8, + "character": 43 + } + }, "value": { "type": "ExpressionTag", "start": 44, @@ -120,12 +204,36 @@ "start": 56, "end": 80, "name": "div", + "name_loc": { + "start": { + "line": 5, + "column": 1, + "character": 57 + }, + "end": { + "line": 5, + "column": 4, + "character": 60 + } + }, "attributes": [ { "type": "Attribute", "start": 61, "end": 73, "name": "foo", + "name_loc": { + "start": { + "line": 5, + "column": 5, + "character": 61 + }, + "end": { + "line": 5, + "column": 8, + "character": 64 + } + }, "value": { "type": "ExpressionTag", "start": 65, @@ -156,12 +264,36 @@ "start": 81, "end": 113, "name": "Component", + "name_loc": { + "start": { + "line": 6, + "column": 1, + "character": 82 + }, + "end": { + "line": 6, + "column": 10, + "character": 91 + } + }, "attributes": [ { "type": "Attribute", "start": 92, "end": 110, "name": "onclick", + "name_loc": { + "start": { + "line": 6, + "column": 11, + "character": 92 + }, + "end": { + "line": 6, + "column": 18, + "character": 99 + } + }, "value": { "type": "ExpressionTag", "start": 100, @@ -192,12 +324,36 @@ "start": 115, "end": 140, "name": "input", + "name_loc": { + "start": { + "line": 8, + "column": 1, + "character": 116 + }, + "end": { + "line": 8, + "column": 6, + "character": 121 + } + }, "attributes": [ { "start": 122, "end": 137, "type": "BindDirective", "name": "value", + "name_loc": { + "start": { + "line": 8, + "column": 7, + "character": 122 + }, + "end": { + "line": 8, + "column": 17, + "character": 132 + } + }, "expression": { "type": "Identifier", "start": 134, @@ -307,6 +463,7 @@ "type": "Identifier", "name": "item", "start": 197, + "end": 201, "loc": { "start": { "line": 15, @@ -318,8 +475,7 @@ "column": 20, "character": 201 } - }, - "end": 201 + } }, "key": { "type": "Identifier", @@ -353,6 +509,7 @@ "type": "Identifier", "name": "item", "start": 234, + "end": 238, "loc": { "start": { "line": 17, @@ -364,8 +521,7 @@ "column": 19, "character": 238 } - }, - "end": 238 + } } }, { @@ -415,6 +571,7 @@ "type": "Identifier", "name": "y", "start": 285, + "end": 286, "loc": { "start": { "line": 21, @@ -426,8 +583,7 @@ "column": 17, "character": 286 } - }, - "end": 286 + } }, "error": null, "pending": null, @@ -459,6 +615,7 @@ "type": "Identifier", "name": "y", "start": 314, + "end": 315, "loc": { "start": { "line": 23, @@ -470,8 +627,7 @@ "column": 18, "character": 315 } - }, - "end": 315 + } }, "pending": null, "then": null, diff --git a/packages/svelte/tests/parser-modern/samples/loose-unclosed-open-tag/output.json b/packages/svelte/tests/parser-modern/samples/loose-unclosed-open-tag/output.json index a0e13d4352..b614e0391d 100644 --- a/packages/svelte/tests/parser-modern/samples/loose-unclosed-open-tag/output.json +++ b/packages/svelte/tests/parser-modern/samples/loose-unclosed-open-tag/output.json @@ -12,6 +12,18 @@ "start": 0, "end": 29, "name": "div", + "name_loc": { + "start": { + "line": 1, + "column": 1, + "character": 1 + }, + "end": { + "line": 1, + "column": 4, + "character": 4 + } + }, "attributes": [], "fragment": { "type": "Fragment", @@ -28,12 +40,36 @@ "start": 7, "end": 23, "name": "Comp", + "name_loc": { + "start": { + "line": 2, + "column": 2, + "character": 8 + }, + "end": { + "line": 2, + "column": 6, + "character": 12 + } + }, "attributes": [ { "type": "Attribute", "start": 13, "end": 22, "name": "foo", + "name_loc": { + "start": { + "line": 2, + "column": 7, + "character": 13 + }, + "end": { + "line": 2, + "column": 10, + "character": 16 + } + }, "value": { "type": "ExpressionTag", "start": 17, @@ -77,6 +113,18 @@ "start": 31, "end": 60, "name": "div", + "name_loc": { + "start": { + "line": 5, + "column": 1, + "character": 32 + }, + "end": { + "line": 5, + "column": 4, + "character": 35 + } + }, "attributes": [], "fragment": { "type": "Fragment", @@ -93,12 +141,36 @@ "start": 38, "end": 54, "name": "span", + "name_loc": { + "start": { + "line": 6, + "column": 2, + "character": 39 + }, + "end": { + "line": 6, + "column": 6, + "character": 43 + } + }, "attributes": [ { "type": "Attribute", "start": 44, "end": 53, "name": "foo", + "name_loc": { + "start": { + "line": 6, + "column": 7, + "character": 44 + }, + "end": { + "line": 6, + "column": 10, + "character": 47 + } + }, "value": { "type": "ExpressionTag", "start": 48, @@ -173,12 +245,36 @@ "start": 73, "end": 89, "name": "Comp", + "name_loc": { + "start": { + "line": 10, + "column": 2, + "character": 74 + }, + "end": { + "line": 10, + "column": 6, + "character": 78 + } + }, "attributes": [ { "type": "Attribute", "start": 79, "end": 88, "name": "foo", + "name_loc": { + "start": { + "line": 10, + "column": 7, + "character": 79 + }, + "end": { + "line": 10, + "column": 10, + "character": 82 + } + }, "value": { "type": "ExpressionTag", "start": 83, @@ -254,12 +350,36 @@ "start": 107, "end": 124, "name": "Comp", + "name_loc": { + "start": { + "line": 14, + "column": 2, + "character": 108 + }, + "end": { + "line": 14, + "column": 6, + "character": 112 + } + }, "attributes": [ { "type": "Attribute", "start": 113, "end": 122, "name": "foo", + "name_loc": { + "start": { + "line": 14, + "column": 7, + "character": 113 + }, + "end": { + "line": 14, + "column": 10, + "character": 116 + } + }, "value": { "type": "ExpressionTag", "start": 117, @@ -374,12 +494,36 @@ "start": 156, "end": 170, "name": "div", + "name_loc": { + "start": { + "line": 20, + "column": 1, + "character": 157 + }, + "end": { + "line": 20, + "column": 4, + "character": 160 + } + }, "attributes": [ { "type": "Attribute", "start": 161, "end": 170, "name": "foo", + "name_loc": { + "start": { + "line": 20, + "column": 5, + "character": 161 + }, + "end": { + "line": 20, + "column": 8, + "character": 164 + } + }, "value": { "type": "ExpressionTag", "start": 165, diff --git a/packages/svelte/tests/parser-modern/samples/loose-unclosed-tag/output.json b/packages/svelte/tests/parser-modern/samples/loose-unclosed-tag/output.json index cf9138c026..59bff2ff80 100644 --- a/packages/svelte/tests/parser-modern/samples/loose-unclosed-tag/output.json +++ b/packages/svelte/tests/parser-modern/samples/loose-unclosed-tag/output.json @@ -12,6 +12,18 @@ "start": 0, "end": 20, "name": "div", + "name_loc": { + "start": { + "line": 1, + "column": 1, + "character": 1 + }, + "end": { + "line": 1, + "column": 4, + "character": 4 + } + }, "attributes": [], "fragment": { "type": "Fragment", @@ -28,6 +40,18 @@ "start": 7, "end": 14, "name": "Comp", + "name_loc": { + "start": { + "line": 2, + "column": 2, + "character": 8 + }, + "end": { + "line": 2, + "column": 6, + "character": 12 + } + }, "attributes": [], "fragment": { "type": "Fragment", @@ -57,6 +81,18 @@ "start": 22, "end": 51, "name": "div", + "name_loc": { + "start": { + "line": 5, + "column": 1, + "character": 23 + }, + "end": { + "line": 5, + "column": 4, + "character": 26 + } + }, "attributes": [], "fragment": { "type": "Fragment", @@ -73,12 +109,36 @@ "start": 29, "end": 45, "name": "Comp", + "name_loc": { + "start": { + "line": 6, + "column": 2, + "character": 30 + }, + "end": { + "line": 6, + "column": 6, + "character": 34 + } + }, "attributes": [ { "type": "Attribute", "start": 35, "end": 44, "name": "foo", + "name_loc": { + "start": { + "line": 6, + "column": 7, + "character": 35 + }, + "end": { + "line": 6, + "column": 10, + "character": 38 + } + }, "value": { "type": "ExpressionTag", "start": 39, @@ -122,6 +182,18 @@ "start": 53, "end": 72, "name": "div", + "name_loc": { + "start": { + "line": 9, + "column": 1, + "character": 54 + }, + "end": { + "line": 9, + "column": 4, + "character": 57 + } + }, "attributes": [], "fragment": { "type": "Fragment", @@ -138,6 +210,18 @@ "start": 60, "end": 66, "name": "span", + "name_loc": { + "start": { + "line": 10, + "column": 2, + "character": 61 + }, + "end": { + "line": 10, + "column": 6, + "character": 65 + } + }, "attributes": [], "fragment": { "type": "Fragment", @@ -159,6 +243,18 @@ "start": 74, "end": 94, "name": "div", + "name_loc": { + "start": { + "line": 13, + "column": 1, + "character": 75 + }, + "end": { + "line": 13, + "column": 4, + "character": 78 + } + }, "attributes": [], "fragment": { "type": "Fragment", @@ -175,6 +271,18 @@ "start": 81, "end": 88, "name": "Comp.", + "name_loc": { + "start": { + "line": 14, + "column": 2, + "character": 82 + }, + "end": { + "line": 14, + "column": 7, + "character": 87 + } + }, "attributes": [], "fragment": { "type": "Fragment", @@ -196,6 +304,18 @@ "start": 96, "end": 116, "name": "div", + "name_loc": { + "start": { + "line": 17, + "column": 1, + "character": 97 + }, + "end": { + "line": 17, + "column": 4, + "character": 100 + } + }, "attributes": [], "fragment": { "type": "Fragment", @@ -212,6 +332,18 @@ "start": 103, "end": 110, "name": "comp.", + "name_loc": { + "start": { + "line": 18, + "column": 2, + "character": 104 + }, + "end": { + "line": 18, + "column": 7, + "character": 109 + } + }, "attributes": [], "fragment": { "type": "Fragment", @@ -264,6 +396,18 @@ "start": 129, "end": 135, "name": "div", + "name_loc": { + "start": { + "line": 22, + "column": 2, + "character": 130 + }, + "end": { + "line": 22, + "column": 5, + "character": 133 + } + }, "attributes": [], "fragment": { "type": "Fragment", @@ -325,12 +469,36 @@ "start": 153, "end": 169, "name": "Comp", + "name_loc": { + "start": { + "line": 26, + "column": 2, + "character": 154 + }, + "end": { + "line": 26, + "column": 6, + "character": 158 + } + }, "attributes": [ { "type": "Attribute", "start": 159, "end": 168, "name": "foo", + "name_loc": { + "start": { + "line": 26, + "column": 7, + "character": 159 + }, + "end": { + "line": 26, + "column": 10, + "character": 162 + } + }, "value": { "type": "ExpressionTag", "start": 163, @@ -375,6 +543,18 @@ "start": 176, "end": 204, "name": "div", + "name_loc": { + "start": { + "line": 29, + "column": 1, + "character": 177 + }, + "end": { + "line": 29, + "column": 4, + "character": 180 + } + }, "attributes": [], "fragment": { "type": "Fragment", @@ -391,6 +571,18 @@ "start": 182, "end": 191, "name": "p", + "name_loc": { + "start": { + "line": 30, + "column": 1, + "character": 183 + }, + "end": { + "line": 30, + "column": 2, + "character": 184 + } + }, "attributes": [], "fragment": { "type": "Fragment", @@ -417,6 +609,18 @@ "start": 193, "end": 204, "name": "open-ended", + "name_loc": { + "start": { + "line": 32, + "column": 1, + "character": 194 + }, + "end": { + "line": 32, + "column": 11, + "character": 204 + } + }, "attributes": [], "fragment": { "type": "Fragment", 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 181f1ba250..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 @@ -1,7 +1,7 @@ { "css": null, "js": [], - "start": 45, + "start": 0, "end": 119, "type": "Root", "fragment": { @@ -49,6 +49,18 @@ "start": 86, "end": 111, "name": "div", + "name_loc": { + "start": { + "line": 6, + "column": 2, + "character": 87 + }, + "end": { + "line": 6, + "column": 5, + "character": 90 + } + }, "attributes": [], "fragment": { "type": "Fragment", @@ -215,7 +227,7 @@ }, "end": { "line": 3, - "column": 0 + "column": 9 } }, "body": [ @@ -293,6 +305,18 @@ "start": 8, "end": 17, "name": "lang", + "name_loc": { + "start": { + "line": 1, + "column": 8, + "character": 8 + }, + "end": { + "line": 1, + "column": 12, + "character": 12 + } + }, "value": [ { "start": 14, diff --git a/packages/svelte/tests/parser-modern/samples/options/output.json b/packages/svelte/tests/parser-modern/samples/options/output.json index 6feee2d4f5..c220811175 100644 --- a/packages/svelte/tests/parser-modern/samples/options/output.json +++ b/packages/svelte/tests/parser-modern/samples/options/output.json @@ -2,7 +2,7 @@ "css": null, "js": [], "start": 0, - "end": 102, + "end": 169, "type": "Root", "fragment": { "type": "Fragment", @@ -32,6 +32,18 @@ "start": 16, "end": 49, "name": "customElement", + "name_loc": { + "start": { + "line": 1, + "column": 16, + "character": 16 + }, + "end": { + "line": 1, + "column": 29, + "character": 29 + } + }, "value": [ { "start": 31, @@ -47,6 +59,18 @@ "start": 50, "end": 62, "name": "runes", + "name_loc": { + "start": { + "line": 1, + "column": 50, + "character": 50 + }, + "end": { + "line": 1, + "column": 55, + "character": 55 + } + }, "value": { "type": "ExpressionTag", "start": 56, @@ -87,12 +111,12 @@ "end": 93, "loc": { "start": { - "line": 1, + "line": 3, "column": 0 }, "end": { "line": 4, - "column": 0 + "column": 9 } }, "body": [], @@ -104,6 +128,18 @@ "start": 75, "end": 81, "name": "module", + "name_loc": { + "start": { + "line": 3, + "column": 8, + "character": 75 + }, + "end": { + "line": 3, + "column": 14, + "character": 81 + } + }, "value": true }, { @@ -111,6 +147,18 @@ "start": 82, "end": 91, "name": "lang", + "name_loc": { + "start": { + "line": 3, + "column": 15, + "character": 82 + }, + "end": { + "line": 3, + "column": 19, + "character": 86 + } + }, "value": [ { "start": 88, @@ -134,12 +182,12 @@ "end": 160, "loc": { "start": { - "line": 1, + "line": 6, "column": 0 }, "end": { "line": 7, - "column": 0 + "column": 9 } }, "body": [], @@ -151,6 +199,18 @@ "start": 112, "end": 121, "name": "lang", + "name_loc": { + "start": { + "line": 6, + "column": 8, + "character": 112 + }, + "end": { + "line": 6, + "column": 12, + "character": 116 + } + }, "value": [ { "start": 118, @@ -166,6 +226,18 @@ "start": 122, "end": 158, "name": "generics", + "name_loc": { + "start": { + "line": 6, + "column": 18, + "character": 122 + }, + "end": { + "line": 6, + "column": 26, + "character": 130 + } + }, "value": [ { "start": 132, diff --git a/packages/svelte/tests/parser-modern/samples/script-style-no-markup/input.svelte b/packages/svelte/tests/parser-modern/samples/script-style-no-markup/input.svelte new file mode 100644 index 0000000000..c4dc4d5271 --- /dev/null +++ b/packages/svelte/tests/parser-modern/samples/script-style-no-markup/input.svelte @@ -0,0 +1,6 @@ + + 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 new file mode 100644 index 0000000000..db58a62148 --- /dev/null +++ b/packages/svelte/tests/parser-modern/samples/script-style-no-markup/output.json @@ -0,0 +1,112 @@ +{ + "css": { + "type": "StyleSheet", + "start": 54, + "end": 91, + "attributes": [], + "children": [ + { + "type": "Rule", + "prelude": { + "type": "SelectorList", + "start": 63, + "end": 66, + "children": [ + { + "type": "ComplexSelector", + "start": 63, + "end": 66, + "children": [ + { + "type": "RelativeSelector", + "combinator": null, + "selectors": [ + { + "type": "TypeSelector", + "name": "div", + "start": 63, + "end": 66 + } + ], + "start": 63, + "end": 66 + } + ] + } + ] + }, + "block": { + "type": "Block", + "start": 67, + "end": 82, + "children": [ + { + "type": "Declaration", + "start": 69, + "end": 79, + "property": "color", + "value": "red" + } + ] + }, + "start": 63, + "end": 82 + } + ], + "content": { + "start": 61, + "end": 83, + "styles": "\n\tdiv { color: red; }\n", + "comment": null + } + }, + "js": [], + "start": 0, + "end": 91, + "type": "Root", + "fragment": { + "type": "Fragment", + "nodes": [ + { + "type": "Text", + "start": 53, + "end": 54, + "raw": "\n", + "data": "\n" + } + ] + }, + "options": null, + "instance": { + "type": "Script", + "start": 0, + "end": 53, + "context": "default", + "content": { + "type": "Program", + "start": 8, + "end": 44, + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 3, + "column": 9 + } + }, + "body": [], + "sourceType": "module", + "trailingComments": [ + { + "type": "Line", + "value": " script and style but no markup", + "start": 10, + "end": 43 + } + ] + }, + "attributes": [] + } +} diff --git a/packages/svelte/tests/parser-modern/samples/semicolon-inside-quotes/output.json b/packages/svelte/tests/parser-modern/samples/semicolon-inside-quotes/output.json index 33dd78879c..312bd7701e 100644 --- a/packages/svelte/tests/parser-modern/samples/semicolon-inside-quotes/output.json +++ b/packages/svelte/tests/parser-modern/samples/semicolon-inside-quotes/output.json @@ -77,7 +77,7 @@ }, "js": [], "start": 0, - "end": 35, + "end": 205, "type": "Root", "fragment": { "type": "Fragment", @@ -87,6 +87,18 @@ "start": 0, "end": 35, "name": "h1", + "name_loc": { + "start": { + "line": 1, + "column": 1, + "character": 1 + }, + "end": { + "line": 1, + "column": 3, + "character": 3 + } + }, "attributes": [], "fragment": { "type": "Fragment", diff --git a/packages/svelte/tests/parser-modern/samples/snippets/output.json b/packages/svelte/tests/parser-modern/samples/snippets/output.json index acf484d8ae..f358e2a60c 100644 --- a/packages/svelte/tests/parser-modern/samples/snippets/output.json +++ b/packages/svelte/tests/parser-modern/samples/snippets/output.json @@ -1,7 +1,7 @@ { "css": null, "js": [], - "start": 29, + "start": 0, "end": 101, "type": "Root", "fragment": { @@ -20,9 +20,21 @@ "end": 81, "expression": { "type": "Identifier", + "name": "foo", "start": 39, "end": 42, - "name": "foo" + "loc": { + "start": { + "line": 3, + "column": 10, + "character": 39 + }, + "end": { + "line": 3, + "column": 13, + "character": 42 + } + } }, "parameters": [ { @@ -87,6 +99,18 @@ "start": 58, "end": 70, "name": "p", + "name_loc": { + "start": { + "line": 4, + "column": 2, + "character": 59 + }, + "end": { + "line": 4, + "column": 3, + "character": 60 + } + }, "attributes": [], "fragment": { "type": "Fragment", @@ -206,7 +230,7 @@ }, "end": { "line": 1, - "column": 18 + "column": 27 } }, "body": [], @@ -218,6 +242,18 @@ "start": 8, "end": 17, "name": "lang", + "name_loc": { + "start": { + "line": 1, + "column": 8, + "character": 8 + }, + "end": { + "line": 1, + "column": 12, + "character": 12 + } + }, "value": [ { "start": 14, diff --git a/packages/svelte/tests/parser-modern/samples/template-shadowroot/output.json b/packages/svelte/tests/parser-modern/samples/template-shadowroot/output.json index 65d716a82b..2cc1dc4988 100644 --- a/packages/svelte/tests/parser-modern/samples/template-shadowroot/output.json +++ b/packages/svelte/tests/parser-modern/samples/template-shadowroot/output.json @@ -12,12 +12,36 @@ "start": 0, "end": 66, "name": "template", + "name_loc": { + "start": { + "line": 1, + "column": 1, + "character": 1 + }, + "end": { + "line": 1, + "column": 9, + "character": 9 + } + }, "attributes": [ { "type": "Attribute", "start": 10, "end": 31, "name": "shadowrootmode", + "name_loc": { + "start": { + "line": 1, + "column": 10, + "character": 10 + }, + "end": { + "line": 1, + "column": 24, + "character": 24 + } + }, "value": [ { "start": 26, @@ -44,6 +68,18 @@ "start": 34, "end": 54, "name": "p", + "name_loc": { + "start": { + "line": 2, + "column": 2, + "character": 35 + }, + "end": { + "line": 2, + "column": 3, + "character": 36 + } + }, "attributes": [], "fragment": { "type": "Fragment", @@ -53,6 +89,18 @@ "start": 37, "end": 50, "name": "slot", + "name_loc": { + "start": { + "line": 2, + "column": 5, + "character": 38 + }, + "end": { + "line": 2, + "column": 9, + "character": 42 + } + }, "attributes": [], "fragment": { "type": "Fragment", @@ -84,6 +132,18 @@ "start": 67, "end": 111, "name": "template", + "name_loc": { + "start": { + "line": 4, + "column": 1, + "character": 68 + }, + "end": { + "line": 4, + "column": 9, + "character": 76 + } + }, "attributes": [], "fragment": { "type": "Fragment", @@ -100,6 +160,18 @@ "start": 79, "end": 99, "name": "p", + "name_loc": { + "start": { + "line": 5, + "column": 2, + "character": 80 + }, + "end": { + "line": 5, + "column": 3, + "character": 81 + } + }, "attributes": [], "fragment": { "type": "Fragment", @@ -109,6 +181,18 @@ "start": 82, "end": 95, "name": "slot", + "name_loc": { + "start": { + "line": 5, + "column": 5, + "character": 83 + }, + "end": { + "line": 5, + "column": 9, + "character": 87 + } + }, "attributes": [], "fragment": { "type": "Fragment", @@ -140,6 +224,18 @@ "start": 112, "end": 125, "name": "slot", + "name_loc": { + "start": { + "line": 7, + "column": 1, + "character": 113 + }, + "end": { + "line": 7, + "column": 5, + "character": 117 + } + }, "attributes": [], "fragment": { "type": "Fragment", 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 9c515ad905..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 @@ -1,7 +1,7 @@ { "css": null, "js": [], - "start": 54, + "start": 0, "end": 173, "type": "Root", "fragment": { @@ -19,12 +19,36 @@ "start": 54, "end": 173, "name": "button", + "name_loc": { + "start": { + "line": 5, + "column": 1, + "character": 55 + }, + "end": { + "line": 5, + "column": 7, + "character": 61 + } + }, "attributes": [ { "start": 63, "end": 147, "type": "OnDirective", "name": "click", + "name_loc": { + "start": { + "line": 6, + "column": 1, + "character": 63 + }, + "end": { + "line": 6, + "column": 9, + "character": 71 + } + }, "expression": { "type": "ArrowFunctionExpression", "start": 73, @@ -372,7 +396,7 @@ }, "end": { "line": 3, - "column": 0 + "column": 9 } }, "body": [ @@ -485,6 +509,18 @@ "start": 8, "end": 17, "name": "lang", + "name_loc": { + "start": { + "line": 1, + "column": 8, + "character": 8 + }, + "end": { + "line": 1, + "column": 12, + "character": 12 + } + }, "value": [ { "start": 14, diff --git a/packages/svelte/tests/parser-modern/test.ts b/packages/svelte/tests/parser-modern/test.ts index 279ba7bc08..fb762e89e1 100644 --- a/packages/svelte/tests/parser-modern/test.ts +++ b/packages/svelte/tests/parser-modern/test.ts @@ -1,12 +1,16 @@ import * as fs from 'node:fs'; import { assert, it } from 'vitest'; -import { parse } from 'svelte/compiler'; +import { parse, print } from 'svelte/compiler'; import { try_load_json } from '../helpers.js'; import { suite, type BaseTest } from '../suite.js'; +import { walk } from 'zimmerframe'; +import type { AST } from 'svelte/compiler'; interface ParserTest extends BaseTest {} const { test, run } = suite(async (config, cwd) => { + const loose = cwd.split('/').pop()!.startsWith('loose-'); + const input = fs .readFileSync(`${cwd}/input.svelte`, 'utf-8') .replace(/\s+$/, '') @@ -25,15 +29,94 @@ const { test, run } = suite(async (config, cwd) => { // run `UPDATE_SNAPSHOTS=true pnpm test parser` to update parser tests if (process.env.UPDATE_SNAPSHOTS) { - fs.writeFileSync(`${cwd}/output.json`, JSON.stringify(actual, null, '\t')); + fs.writeFileSync(`${cwd}/output.json`, JSON.stringify(actual, null, '\t') + '\n'); } else { fs.writeFileSync(`${cwd}/_actual.json`, JSON.stringify(actual, null, '\t')); const expected = try_load_json(`${cwd}/output.json`); assert.deepEqual(actual, expected); } + + if (!loose) { + const printed = print(actual); + const reparsed = JSON.parse( + JSON.stringify( + parse(printed.code, { + modern: true, + loose + }) + ) + ); + + fs.writeFileSync(`${cwd}/_actual.svelte`, printed.code); + + delete reparsed.comments; + + assert.deepEqual(clean(actual), clean(reparsed)); + } }); +function clean(ast: AST.SvelteNode) { + return walk(ast, null, { + _(node, context) { + // @ts-ignore + delete node.start; + // @ts-ignore + delete node.end; + // @ts-ignore + delete node.loc; + // @ts-ignore + delete node.name_loc; + // @ts-ignore + delete node.leadingComments; + // @ts-ignore + delete node.trailingComments; + + context.next(); + }, + StyleSheet(node, context) { + return { + type: node.type, + attributes: node.attributes.map((attribute) => context.visit(attribute)), + children: node.children.map((child) => context.visit(child)), + content: {} + } as AST.SvelteNode; + }, + Fragment(node, context) { + const nodes: AST.SvelteNode[] = []; + + for (let i = 0; i < node.nodes.length; i += 1) { + let child = node.nodes[i]; + + if (child.type === 'Text') { + child = { + ...child, + // trim multiple whitespace to single space + data: child.data.replace(/[^\S]+/g, ' '), + raw: child.raw.replace(/[^\S]+/g, ' ') + }; + + if (i === 0) { + child.data = child.data.trimStart(); + child.raw = child.raw.trimStart(); + } + + if (i === node.nodes.length - 1) { + child.data = child.data.trimEnd(); + child.raw = child.raw.trimEnd(); + } + + if (child.data === '') continue; + } + + nodes.push(context.visit(child)); + } + + return { ...node, nodes } as AST.Fragment; + } + }); +} + export { test }; await run(__dirname); @@ -51,6 +134,18 @@ it('Strips BOM from the input', () => { nodes: [], type: 'Fragment' }, + name_loc: { + end: { + character: 4, + column: 4, + line: 1 + }, + start: { + character: 1, + column: 1, + line: 1 + } + }, name: 'div', start: 0, type: 'RegularElement' diff --git a/packages/svelte/tests/print/samples/animate-directive/input.svelte b/packages/svelte/tests/print/samples/animate-directive/input.svelte new file mode 100644 index 0000000000..7cd314f714 --- /dev/null +++ b/packages/svelte/tests/print/samples/animate-directive/input.svelte @@ -0,0 +1 @@ +
{item}
diff --git a/packages/svelte/tests/print/samples/animate-directive/output.svelte b/packages/svelte/tests/print/samples/animate-directive/output.svelte new file mode 100644 index 0000000000..7cd314f714 --- /dev/null +++ b/packages/svelte/tests/print/samples/animate-directive/output.svelte @@ -0,0 +1 @@ +
{item}
diff --git a/packages/svelte/tests/print/samples/attach-tag/input.svelte b/packages/svelte/tests/print/samples/attach-tag/input.svelte new file mode 100644 index 0000000000..e92604b92b --- /dev/null +++ b/packages/svelte/tests/print/samples/attach-tag/input.svelte @@ -0,0 +1,13 @@ + + +
...
diff --git a/packages/svelte/tests/print/samples/attach-tag/output.svelte b/packages/svelte/tests/print/samples/attach-tag/output.svelte new file mode 100644 index 0000000000..e92604b92b --- /dev/null +++ b/packages/svelte/tests/print/samples/attach-tag/output.svelte @@ -0,0 +1,13 @@ + + +
...
diff --git a/packages/svelte/tests/print/samples/attribute/input.svelte b/packages/svelte/tests/print/samples/attribute/input.svelte new file mode 100644 index 0000000000..4da846f8e6 --- /dev/null +++ b/packages/svelte/tests/print/samples/attribute/input.svelte @@ -0,0 +1 @@ +
diff --git a/packages/svelte/tests/print/samples/attribute/output.svelte b/packages/svelte/tests/print/samples/attribute/output.svelte new file mode 100644 index 0000000000..4da846f8e6 --- /dev/null +++ b/packages/svelte/tests/print/samples/attribute/output.svelte @@ -0,0 +1 @@ +
diff --git a/packages/svelte/tests/print/samples/await-block/input.svelte b/packages/svelte/tests/print/samples/await-block/input.svelte new file mode 100644 index 0000000000..10f0b3fb9e --- /dev/null +++ b/packages/svelte/tests/print/samples/await-block/input.svelte @@ -0,0 +1,10 @@ +{#await promise} + +

waiting for the promise to resolve...

+{:then value} + +

The value is {value}

+{:catch error} + +

Something went wrong: {error.message}

+{/await} diff --git a/packages/svelte/tests/print/samples/await-block/output.svelte b/packages/svelte/tests/print/samples/await-block/output.svelte new file mode 100644 index 0000000000..10f0b3fb9e --- /dev/null +++ b/packages/svelte/tests/print/samples/await-block/output.svelte @@ -0,0 +1,10 @@ +{#await promise} + +

waiting for the promise to resolve...

+{:then value} + +

The value is {value}

+{:catch error} + +

Something went wrong: {error.message}

+{/await} diff --git a/packages/svelte/tests/print/samples/bind-directive/input.svelte b/packages/svelte/tests/print/samples/bind-directive/input.svelte new file mode 100644 index 0000000000..3b7e08921e --- /dev/null +++ b/packages/svelte/tests/print/samples/bind-directive/input.svelte @@ -0,0 +1 @@ + diff --git a/packages/svelte/tests/print/samples/bind-directive/output.svelte b/packages/svelte/tests/print/samples/bind-directive/output.svelte new file mode 100644 index 0000000000..3b7e08921e --- /dev/null +++ b/packages/svelte/tests/print/samples/bind-directive/output.svelte @@ -0,0 +1 @@ + diff --git a/packages/svelte/tests/print/samples/block/input.svelte b/packages/svelte/tests/print/samples/block/input.svelte new file mode 100644 index 0000000000..84140c5c02 --- /dev/null +++ b/packages/svelte/tests/print/samples/block/input.svelte @@ -0,0 +1,9 @@ +{#if condition} yes {:else} no {/if} + +{#each items as item, i} +

{i}: {item}

+{/each} + +{#if condition}yes{:else}no{/if} + +{#each items as item, i}

{i}: {item}

{/each} diff --git a/packages/svelte/tests/print/samples/block/output.svelte b/packages/svelte/tests/print/samples/block/output.svelte new file mode 100644 index 0000000000..e5d8590875 --- /dev/null +++ b/packages/svelte/tests/print/samples/block/output.svelte @@ -0,0 +1,19 @@ +{#if condition} + yes +{:else} + no +{/if} + +{#each items as item, i} +

{i}: {item}

+{/each} + +{#if condition} + yes +{:else} + no +{/if} + +{#each items as item, i} +

{i}: {item}

+{/each} diff --git a/packages/svelte/tests/print/samples/class-directive/input.svelte b/packages/svelte/tests/print/samples/class-directive/input.svelte new file mode 100644 index 0000000000..5e63111930 --- /dev/null +++ b/packages/svelte/tests/print/samples/class-directive/input.svelte @@ -0,0 +1,8 @@ + + +
+ Hello world! +
diff --git a/packages/svelte/tests/print/samples/class-directive/output.svelte b/packages/svelte/tests/print/samples/class-directive/output.svelte new file mode 100644 index 0000000000..14e5611331 --- /dev/null +++ b/packages/svelte/tests/print/samples/class-directive/output.svelte @@ -0,0 +1,6 @@ + + +
Hello world!
diff --git a/packages/svelte/tests/print/samples/comment/input.svelte b/packages/svelte/tests/print/samples/comment/input.svelte new file mode 100644 index 0000000000..d144ced1b9 --- /dev/null +++ b/packages/svelte/tests/print/samples/comment/input.svelte @@ -0,0 +1,5 @@ + + + diff --git a/packages/svelte/tests/print/samples/comment/output.svelte b/packages/svelte/tests/print/samples/comment/output.svelte new file mode 100644 index 0000000000..f2f97b65be --- /dev/null +++ b/packages/svelte/tests/print/samples/comment/output.svelte @@ -0,0 +1,3 @@ + diff --git a/packages/svelte/tests/print/samples/component/input.svelte b/packages/svelte/tests/print/samples/component/input.svelte new file mode 100644 index 0000000000..c258d7d609 --- /dev/null +++ b/packages/svelte/tests/print/samples/component/input.svelte @@ -0,0 +1,8 @@ + + + + + Hello World + diff --git a/packages/svelte/tests/print/samples/component/output.svelte b/packages/svelte/tests/print/samples/component/output.svelte new file mode 100644 index 0000000000..ec8b827883 --- /dev/null +++ b/packages/svelte/tests/print/samples/component/output.svelte @@ -0,0 +1,6 @@ + + + +Hello World diff --git a/packages/svelte/tests/print/samples/const-tag/input.svelte b/packages/svelte/tests/print/samples/const-tag/input.svelte new file mode 100644 index 0000000000..50ada2dd66 --- /dev/null +++ b/packages/svelte/tests/print/samples/const-tag/input.svelte @@ -0,0 +1,8 @@ + + +{#each boxes as box} + {@const area = box.width * box.height} + {box.width} * {box.height} = {area} +{/each} diff --git a/packages/svelte/tests/print/samples/const-tag/output.svelte b/packages/svelte/tests/print/samples/const-tag/output.svelte new file mode 100644 index 0000000000..d9d8addcbb --- /dev/null +++ b/packages/svelte/tests/print/samples/const-tag/output.svelte @@ -0,0 +1,8 @@ + + +{#each boxes as box} + {@const area = box.width * box.height;} + {box.width} * {box.height} = {area} +{/each} diff --git a/packages/svelte/tests/print/samples/each-block/input.svelte b/packages/svelte/tests/print/samples/each-block/input.svelte new file mode 100644 index 0000000000..a859705fec --- /dev/null +++ b/packages/svelte/tests/print/samples/each-block/input.svelte @@ -0,0 +1,15 @@ +{#each items as { id, name, qty }, i (id)} +
  • {i + 1}: {name} x {qty}
  • +{/each} + +{#each objects as { id, ...rest }} +
  • {id}
  • +{/each} + +{#each expression}...{/each} + +{#each todos as todo} +

    {todo.text}

    +{:else} +

    No tasks today!

    +{/each} diff --git a/packages/svelte/tests/print/samples/each-block/output.svelte b/packages/svelte/tests/print/samples/each-block/output.svelte new file mode 100644 index 0000000000..b19925a694 --- /dev/null +++ b/packages/svelte/tests/print/samples/each-block/output.svelte @@ -0,0 +1,17 @@ +{#each items as { id, name, qty }, i (id)} +
  • {i + 1}: {name} x {qty}
  • +{/each} + +{#each objects as { id, ...rest }} +
  • {id}
  • +{/each} + +{#each expression} + ... +{/each} + +{#each todos as todo} +

    {todo.text}

    +{:else} +

    No tasks today!

    +{/each} diff --git a/packages/svelte/tests/print/samples/expression-tag/input.svelte b/packages/svelte/tests/print/samples/expression-tag/input.svelte new file mode 100644 index 0000000000..3920c3b40a --- /dev/null +++ b/packages/svelte/tests/print/samples/expression-tag/input.svelte @@ -0,0 +1,2 @@ +{name} +{count + 1} diff --git a/packages/svelte/tests/print/samples/expression-tag/output.svelte b/packages/svelte/tests/print/samples/expression-tag/output.svelte new file mode 100644 index 0000000000..9142a59631 --- /dev/null +++ b/packages/svelte/tests/print/samples/expression-tag/output.svelte @@ -0,0 +1 @@ +{name}{count + 1} diff --git a/packages/svelte/tests/print/samples/formatting/input.svelte b/packages/svelte/tests/print/samples/formatting/input.svelte new file mode 100644 index 0000000000..9b1898e9c8 --- /dev/null +++ b/packages/svelte/tests/print/samples/formatting/input.svelte @@ -0,0 +1 @@ +

    {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 new file mode 100644 index 0000000000..7b40642580 --- /dev/null +++ b/packages/svelte/tests/print/samples/formatting/output.svelte @@ -0,0 +1,21 @@ + + +

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

    +
    + + +
    +

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

    diff --git a/packages/svelte/tests/print/samples/html-document/input.svelte b/packages/svelte/tests/print/samples/html-document/input.svelte new file mode 100644 index 0000000000..8f1b4accab --- /dev/null +++ b/packages/svelte/tests/print/samples/html-document/input.svelte @@ -0,0 +1,11 @@ + + + + + + Svelte App + + +
    Hello World
    + + diff --git a/packages/svelte/tests/print/samples/html-document/output.svelte b/packages/svelte/tests/print/samples/html-document/output.svelte new file mode 100644 index 0000000000..765f9ca84b --- /dev/null +++ b/packages/svelte/tests/print/samples/html-document/output.svelte @@ -0,0 +1,12 @@ + + + + + + Svelte App + +
    Hello World
    + diff --git a/packages/svelte/tests/print/samples/html-tag/input.svelte b/packages/svelte/tests/print/samples/html-tag/input.svelte new file mode 100644 index 0000000000..d021ee8fa6 --- /dev/null +++ b/packages/svelte/tests/print/samples/html-tag/input.svelte @@ -0,0 +1,3 @@ +
    + {@html content} +
    diff --git a/packages/svelte/tests/print/samples/html-tag/output.svelte b/packages/svelte/tests/print/samples/html-tag/output.svelte new file mode 100644 index 0000000000..84777d9b6a --- /dev/null +++ b/packages/svelte/tests/print/samples/html-tag/output.svelte @@ -0,0 +1 @@ +
    {@html content}
    diff --git a/packages/svelte/tests/print/samples/if-block/input.svelte b/packages/svelte/tests/print/samples/if-block/input.svelte new file mode 100644 index 0000000000..0a444a1252 --- /dev/null +++ b/packages/svelte/tests/print/samples/if-block/input.svelte @@ -0,0 +1,7 @@ +{#if porridge.temperature > 100} +

    too hot!

    +{:else if 80 > porridge.temperature} +

    too cold!

    +{:else} +

    just right!

    +{/if} diff --git a/packages/svelte/tests/print/samples/if-block/output.svelte b/packages/svelte/tests/print/samples/if-block/output.svelte new file mode 100644 index 0000000000..0a444a1252 --- /dev/null +++ b/packages/svelte/tests/print/samples/if-block/output.svelte @@ -0,0 +1,7 @@ +{#if porridge.temperature > 100} +

    too hot!

    +{:else if 80 > porridge.temperature} +

    too cold!

    +{:else} +

    just right!

    +{/if} diff --git a/packages/svelte/tests/print/samples/key-block/input.svelte b/packages/svelte/tests/print/samples/key-block/input.svelte new file mode 100644 index 0000000000..9d09f8f00a --- /dev/null +++ b/packages/svelte/tests/print/samples/key-block/input.svelte @@ -0,0 +1,3 @@ +{#key value} + +{/key} diff --git a/packages/svelte/tests/print/samples/key-block/output.svelte b/packages/svelte/tests/print/samples/key-block/output.svelte new file mode 100644 index 0000000000..9d09f8f00a --- /dev/null +++ b/packages/svelte/tests/print/samples/key-block/output.svelte @@ -0,0 +1,3 @@ +{#key value} + +{/key} diff --git a/packages/svelte/tests/print/samples/let-directive/input.svelte b/packages/svelte/tests/print/samples/let-directive/input.svelte new file mode 100644 index 0000000000..c6b49c1561 --- /dev/null +++ b/packages/svelte/tests/print/samples/let-directive/input.svelte @@ -0,0 +1,3 @@ + +
    {processed.text}
    +
    diff --git a/packages/svelte/tests/print/samples/let-directive/output.svelte b/packages/svelte/tests/print/samples/let-directive/output.svelte new file mode 100644 index 0000000000..c7254150e3 --- /dev/null +++ b/packages/svelte/tests/print/samples/let-directive/output.svelte @@ -0,0 +1 @@ +
    {processed.text}
    diff --git a/packages/svelte/tests/print/samples/on-directive/input.svelte b/packages/svelte/tests/print/samples/on-directive/input.svelte new file mode 100644 index 0000000000..976749696a --- /dev/null +++ b/packages/svelte/tests/print/samples/on-directive/input.svelte @@ -0,0 +1,11 @@ + + + diff --git a/packages/svelte/tests/print/samples/on-directive/output.svelte b/packages/svelte/tests/print/samples/on-directive/output.svelte new file mode 100644 index 0000000000..da0945678d --- /dev/null +++ b/packages/svelte/tests/print/samples/on-directive/output.svelte @@ -0,0 +1,9 @@ + + + diff --git a/packages/svelte/tests/print/samples/regular-element/input.svelte b/packages/svelte/tests/print/samples/regular-element/input.svelte new file mode 100644 index 0000000000..c39f4f487f --- /dev/null +++ b/packages/svelte/tests/print/samples/regular-element/input.svelte @@ -0,0 +1,2 @@ + +
    diff --git a/packages/svelte/tests/print/samples/regular-element/output.svelte b/packages/svelte/tests/print/samples/regular-element/output.svelte new file mode 100644 index 0000000000..0cf7c2472f --- /dev/null +++ b/packages/svelte/tests/print/samples/regular-element/output.svelte @@ -0,0 +1 @@ +
    diff --git a/packages/svelte/tests/print/samples/render-tag/input.svelte b/packages/svelte/tests/print/samples/render-tag/input.svelte new file mode 100644 index 0000000000..b301e4b0d7 --- /dev/null +++ b/packages/svelte/tests/print/samples/render-tag/input.svelte @@ -0,0 +1,7 @@ +{#snippet sum(a, b)} +

    {a} + {b} = {a + b}

    +{/snippet} + +{@render sum(1, 2)} +{@render sum(3, 4)} +{@render sum(5, 6)} diff --git a/packages/svelte/tests/print/samples/render-tag/output.svelte b/packages/svelte/tests/print/samples/render-tag/output.svelte new file mode 100644 index 0000000000..b301e4b0d7 --- /dev/null +++ b/packages/svelte/tests/print/samples/render-tag/output.svelte @@ -0,0 +1,7 @@ +{#snippet sum(a, b)} +

    {a} + {b} = {a + b}

    +{/snippet} + +{@render sum(1, 2)} +{@render sum(3, 4)} +{@render sum(5, 6)} diff --git a/packages/svelte/tests/print/samples/script/input.svelte b/packages/svelte/tests/print/samples/script/input.svelte new file mode 100644 index 0000000000..f094ccf550 --- /dev/null +++ b/packages/svelte/tests/print/samples/script/input.svelte @@ -0,0 +1,3 @@ + diff --git a/packages/svelte/tests/print/samples/script/output.svelte b/packages/svelte/tests/print/samples/script/output.svelte new file mode 100644 index 0000000000..da5f5725bf --- /dev/null +++ b/packages/svelte/tests/print/samples/script/output.svelte @@ -0,0 +1,3 @@ + diff --git a/packages/svelte/tests/print/samples/slot-element/input.svelte b/packages/svelte/tests/print/samples/slot-element/input.svelte new file mode 100644 index 0000000000..b92372f984 --- /dev/null +++ b/packages/svelte/tests/print/samples/slot-element/input.svelte @@ -0,0 +1,3 @@ + diff --git a/packages/svelte/tests/print/samples/slot-element/output.svelte b/packages/svelte/tests/print/samples/slot-element/output.svelte new file mode 100644 index 0000000000..4d59994078 --- /dev/null +++ b/packages/svelte/tests/print/samples/slot-element/output.svelte @@ -0,0 +1 @@ + diff --git a/packages/svelte/tests/print/samples/snippet-block/input.svelte b/packages/svelte/tests/print/samples/snippet-block/input.svelte new file mode 100644 index 0000000000..47f8efd9e9 --- /dev/null +++ b/packages/svelte/tests/print/samples/snippet-block/input.svelte @@ -0,0 +1,3 @@ +{#snippet name(param1, param2, paramN)} + Foo +{/snippet} diff --git a/packages/svelte/tests/print/samples/snippet-block/output.svelte b/packages/svelte/tests/print/samples/snippet-block/output.svelte new file mode 100644 index 0000000000..bb172f1db6 --- /dev/null +++ b/packages/svelte/tests/print/samples/snippet-block/output.svelte @@ -0,0 +1,3 @@ +{#snippet name(param1, param2, paramN)} + Foo +{/snippet} diff --git a/packages/svelte/tests/print/samples/spread-attribute/input.svelte b/packages/svelte/tests/print/samples/spread-attribute/input.svelte new file mode 100644 index 0000000000..836425cae3 --- /dev/null +++ b/packages/svelte/tests/print/samples/spread-attribute/input.svelte @@ -0,0 +1 @@ +
    diff --git a/packages/svelte/tests/print/samples/spread-attribute/output.svelte b/packages/svelte/tests/print/samples/spread-attribute/output.svelte new file mode 100644 index 0000000000..836425cae3 --- /dev/null +++ b/packages/svelte/tests/print/samples/spread-attribute/output.svelte @@ -0,0 +1 @@ +
    diff --git a/packages/svelte/tests/print/samples/style-directive/input.svelte b/packages/svelte/tests/print/samples/style-directive/input.svelte new file mode 100644 index 0000000000..2951fc9daa --- /dev/null +++ b/packages/svelte/tests/print/samples/style-directive/input.svelte @@ -0,0 +1,2 @@ +
    ...
    +
    ...
    diff --git a/packages/svelte/tests/print/samples/style-directive/output.svelte b/packages/svelte/tests/print/samples/style-directive/output.svelte new file mode 100644 index 0000000000..5aa3a6dcdb --- /dev/null +++ b/packages/svelte/tests/print/samples/style-directive/output.svelte @@ -0,0 +1,8 @@ +
    ...
    +
    + ... +
    diff --git a/packages/svelte/tests/print/samples/style/input.svelte b/packages/svelte/tests/print/samples/style/input.svelte new file mode 100644 index 0000000000..afa00bee7a --- /dev/null +++ b/packages/svelte/tests/print/samples/style/input.svelte @@ -0,0 +1,54 @@ + diff --git a/packages/svelte/tests/print/samples/style/output.svelte b/packages/svelte/tests/print/samples/style/output.svelte new file mode 100644 index 0000000000..03168cccae --- /dev/null +++ b/packages/svelte/tests/print/samples/style/output.svelte @@ -0,0 +1,91 @@ + + + diff --git a/packages/svelte/tests/print/samples/svelte-boundary/input.svelte b/packages/svelte/tests/print/samples/svelte-boundary/input.svelte new file mode 100644 index 0000000000..2cbd32daf0 --- /dev/null +++ b/packages/svelte/tests/print/samples/svelte-boundary/input.svelte @@ -0,0 +1,7 @@ + +

    {await delayed('hello!')}

    + + {#snippet pending()} +

    loading...

    + {/snippet} +
    diff --git a/packages/svelte/tests/print/samples/svelte-boundary/output.svelte b/packages/svelte/tests/print/samples/svelte-boundary/output.svelte new file mode 100644 index 0000000000..2cbd32daf0 --- /dev/null +++ b/packages/svelte/tests/print/samples/svelte-boundary/output.svelte @@ -0,0 +1,7 @@ + +

    {await delayed('hello!')}

    + + {#snippet pending()} +

    loading...

    + {/snippet} +
    diff --git a/packages/svelte/tests/print/samples/svelte-component/input.svelte b/packages/svelte/tests/print/samples/svelte-component/input.svelte new file mode 100644 index 0000000000..046ee0f893 --- /dev/null +++ b/packages/svelte/tests/print/samples/svelte-component/input.svelte @@ -0,0 +1 @@ + diff --git a/packages/svelte/tests/print/samples/svelte-component/output.svelte b/packages/svelte/tests/print/samples/svelte-component/output.svelte new file mode 100644 index 0000000000..046ee0f893 --- /dev/null +++ b/packages/svelte/tests/print/samples/svelte-component/output.svelte @@ -0,0 +1 @@ + diff --git a/packages/svelte/tests/print/samples/svelte-document/input.svelte b/packages/svelte/tests/print/samples/svelte-document/input.svelte new file mode 100644 index 0000000000..b99b973e0d --- /dev/null +++ b/packages/svelte/tests/print/samples/svelte-document/input.svelte @@ -0,0 +1 @@ + diff --git a/packages/svelte/tests/print/samples/svelte-document/output.svelte b/packages/svelte/tests/print/samples/svelte-document/output.svelte new file mode 100644 index 0000000000..418ae4e008 --- /dev/null +++ b/packages/svelte/tests/print/samples/svelte-document/output.svelte @@ -0,0 +1,4 @@ + diff --git a/packages/svelte/tests/print/samples/svelte-element/input.svelte b/packages/svelte/tests/print/samples/svelte-element/input.svelte new file mode 100644 index 0000000000..388c5a6090 --- /dev/null +++ b/packages/svelte/tests/print/samples/svelte-element/input.svelte @@ -0,0 +1,7 @@ + + + + This text cannot appear inside an hr element + diff --git a/packages/svelte/tests/print/samples/svelte-element/output.svelte b/packages/svelte/tests/print/samples/svelte-element/output.svelte new file mode 100644 index 0000000000..388c5a6090 --- /dev/null +++ b/packages/svelte/tests/print/samples/svelte-element/output.svelte @@ -0,0 +1,7 @@ + + + + This text cannot appear inside an hr element + diff --git a/packages/svelte/tests/print/samples/svelte-fragment/input.svelte b/packages/svelte/tests/print/samples/svelte-fragment/input.svelte new file mode 100644 index 0000000000..eb80023626 --- /dev/null +++ b/packages/svelte/tests/print/samples/svelte-fragment/input.svelte @@ -0,0 +1,11 @@ + + + +

    Hello

    + +

    All rights reserved.

    +

    Copyright (c) 2019 Svelte Industries

    +
    +
    diff --git a/packages/svelte/tests/print/samples/svelte-fragment/output.svelte b/packages/svelte/tests/print/samples/svelte-fragment/output.svelte new file mode 100644 index 0000000000..eb80023626 --- /dev/null +++ b/packages/svelte/tests/print/samples/svelte-fragment/output.svelte @@ -0,0 +1,11 @@ + + + +

    Hello

    + +

    All rights reserved.

    +

    Copyright (c) 2019 Svelte Industries

    +
    +
    diff --git a/packages/svelte/tests/print/samples/svelte-head/input.svelte b/packages/svelte/tests/print/samples/svelte-head/input.svelte new file mode 100644 index 0000000000..0a08871aba --- /dev/null +++ b/packages/svelte/tests/print/samples/svelte-head/input.svelte @@ -0,0 +1,4 @@ + + Hello world! + + diff --git a/packages/svelte/tests/print/samples/svelte-head/output.svelte b/packages/svelte/tests/print/samples/svelte-head/output.svelte new file mode 100644 index 0000000000..68d352260e --- /dev/null +++ b/packages/svelte/tests/print/samples/svelte-head/output.svelte @@ -0,0 +1,7 @@ + + Hello world! + + diff --git a/packages/svelte/tests/print/samples/svelte-options/input.svelte b/packages/svelte/tests/print/samples/svelte-options/input.svelte new file mode 100644 index 0000000000..70e2dda5c2 --- /dev/null +++ b/packages/svelte/tests/print/samples/svelte-options/input.svelte @@ -0,0 +1 @@ + diff --git a/packages/svelte/tests/print/samples/svelte-options/output.svelte b/packages/svelte/tests/print/samples/svelte-options/output.svelte new file mode 100644 index 0000000000..70e2dda5c2 --- /dev/null +++ b/packages/svelte/tests/print/samples/svelte-options/output.svelte @@ -0,0 +1 @@ + diff --git a/packages/svelte/tests/print/samples/svelte-self/input.svelte b/packages/svelte/tests/print/samples/svelte-self/input.svelte new file mode 100644 index 0000000000..7711defef1 --- /dev/null +++ b/packages/svelte/tests/print/samples/svelte-self/input.svelte @@ -0,0 +1,10 @@ + + +{#if count > 0} +

    counting down... {count}

    + +{:else} +

    lift-off!

    +{/if} diff --git a/packages/svelte/tests/print/samples/svelte-self/output.svelte b/packages/svelte/tests/print/samples/svelte-self/output.svelte new file mode 100644 index 0000000000..15031a39fa --- /dev/null +++ b/packages/svelte/tests/print/samples/svelte-self/output.svelte @@ -0,0 +1,10 @@ + + +{#if count > 0} +

    counting down... {count}

    + +{:else} +

    lift-off!

    +{/if} diff --git a/packages/svelte/tests/print/samples/svelte-window/input.svelte b/packages/svelte/tests/print/samples/svelte-window/input.svelte new file mode 100644 index 0000000000..054e584e19 --- /dev/null +++ b/packages/svelte/tests/print/samples/svelte-window/input.svelte @@ -0,0 +1,7 @@ + + + diff --git a/packages/svelte/tests/print/samples/svelte-window/output.svelte b/packages/svelte/tests/print/samples/svelte-window/output.svelte new file mode 100644 index 0000000000..03997acab1 --- /dev/null +++ b/packages/svelte/tests/print/samples/svelte-window/output.svelte @@ -0,0 +1,7 @@ + + + diff --git a/packages/svelte/tests/print/samples/text/input.svelte b/packages/svelte/tests/print/samples/text/input.svelte new file mode 100644 index 0000000000..a04fa3c48c --- /dev/null +++ b/packages/svelte/tests/print/samples/text/input.svelte @@ -0,0 +1 @@ +

    Hello world

    diff --git a/packages/svelte/tests/print/samples/text/output.svelte b/packages/svelte/tests/print/samples/text/output.svelte new file mode 100644 index 0000000000..a04fa3c48c --- /dev/null +++ b/packages/svelte/tests/print/samples/text/output.svelte @@ -0,0 +1 @@ +

    Hello world

    diff --git a/packages/svelte/tests/print/samples/transition-directive/input.svelte b/packages/svelte/tests/print/samples/transition-directive/input.svelte new file mode 100644 index 0000000000..7fd08df6ef --- /dev/null +++ b/packages/svelte/tests/print/samples/transition-directive/input.svelte @@ -0,0 +1,11 @@ + + + + +{#if visible} +
    fades in and out
    +{/if} diff --git a/packages/svelte/tests/print/samples/transition-directive/output.svelte b/packages/svelte/tests/print/samples/transition-directive/output.svelte new file mode 100644 index 0000000000..7fd08df6ef --- /dev/null +++ b/packages/svelte/tests/print/samples/transition-directive/output.svelte @@ -0,0 +1,11 @@ + + + + +{#if visible} +
    fades in and out
    +{/if} diff --git a/packages/svelte/tests/print/samples/use-directive/input.svelte b/packages/svelte/tests/print/samples/use-directive/input.svelte new file mode 100644 index 0000000000..3b9f5562fb --- /dev/null +++ b/packages/svelte/tests/print/samples/use-directive/input.svelte @@ -0,0 +1,9 @@ + + +
    ...
    diff --git a/packages/svelte/tests/print/samples/use-directive/output.svelte b/packages/svelte/tests/print/samples/use-directive/output.svelte new file mode 100644 index 0000000000..3b9f5562fb --- /dev/null +++ b/packages/svelte/tests/print/samples/use-directive/output.svelte @@ -0,0 +1,9 @@ + + +
    ...
    diff --git a/packages/svelte/tests/print/test.ts b/packages/svelte/tests/print/test.ts new file mode 100644 index 0000000000..aa007a7a54 --- /dev/null +++ b/packages/svelte/tests/print/test.ts @@ -0,0 +1,30 @@ +import * as fs from 'node:fs'; +import { assert } from 'vitest'; +import { parse, print } from 'svelte/compiler'; +import { suite, type BaseTest } from '../suite.js'; + +interface PrintTest extends BaseTest {} + +const { test, run } = suite(async (config, cwd) => { + const input = fs.readFileSync(`${cwd}/input.svelte`, 'utf-8'); + + const ast = parse(input, { modern: true }); + const output = print(ast); + const outputCode = output.code.endsWith('\n') ? output.code : output.code + '\n'; + + // run `UPDATE_SNAPSHOTS=true pnpm test print` to update print tests + if (process.env.UPDATE_SNAPSHOTS) { + fs.writeFileSync(`${cwd}/output.svelte`, outputCode); + } else { + fs.writeFileSync(`${cwd}/_actual.svelte`, outputCode); + + const file = `${cwd}/output.svelte`; + + const expected = fs.existsSync(file) ? fs.readFileSync(file, 'utf-8') : ''; + assert.deepEqual(outputCode.trim().replaceAll('\r', ''), expected.trim().replaceAll('\r', '')); + } +}); + +export { test }; + +await run(__dirname); diff --git a/packages/svelte/tests/runtime-browser/assert.js b/packages/svelte/tests/runtime-browser/assert.js index e331c8b677..249c5ad33d 100644 --- a/packages/svelte/tests/runtime-browser/assert.js +++ b/packages/svelte/tests/runtime-browser/assert.js @@ -166,7 +166,7 @@ export function test(args) { return args; } -// TypeScript needs the type of assertions to be directly visible, not infered, which is why +// TypeScript needs the type of assertions to be directly visible, not inferred, which is why // we can't have it on the test suite type. /** * @param {any} value diff --git a/packages/svelte/tests/runtime-browser/custom-elements-samples/closed-shadow-dom/_config.js b/packages/svelte/tests/runtime-browser/custom-elements-samples/closed-shadow-dom/_config.js new file mode 100644 index 0000000000..9e09967dab --- /dev/null +++ b/packages/svelte/tests/runtime-browser/custom-elements-samples/closed-shadow-dom/_config.js @@ -0,0 +1,13 @@ +import { test } from '../../assert'; +const tick = () => 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/test.ts b/packages/svelte/tests/runtime-browser/test.ts index 63e601b115..597b2909dc 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' }); 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/shared.ts b/packages/svelte/tests/runtime-legacy/shared.ts index 7144543242..13975c68ee 100644 --- a/packages/svelte/tests/runtime-legacy/shared.ts +++ b/packages/svelte/tests/runtime-legacy/shared.ts @@ -5,13 +5,14 @@ import { createClassComponent } from 'svelte/legacy'; import { proxy } from 'svelte/internal/client'; import { flushSync, hydrate, mount, unmount } from 'svelte'; import { render } from 'svelte/server'; -import { afterAll, assert, beforeAll } from 'vitest'; +import { afterAll, assert, beforeAll, beforeEach } from 'vitest'; import { async_mode, compile_directory, fragments } from '../helpers.js'; import { assert_html_equal, assert_html_equal_with_options } from '../html_equal.js'; import { raf } from '../animation-helpers.js'; import type { CompileOptions } from '#compiler'; import { suite_with_variants, type BaseTest } from '../suite.js'; import { clear } from '../../src/internal/client/reactivity/batch.js'; +import { hydrating } from '../../src/internal/client/dom/hydration.js'; type Assert = typeof import('vitest').assert & { htmlEqual(a: string, b: string, description?: string): void; @@ -85,6 +86,7 @@ export interface RuntimeTest = Record void | Promise; test_ssr?: (args: { logs: any[]; + warnings: any[]; assert: Assert; variant: 'ssr' | 'async-ssr'; }) => void | Promise; @@ -101,6 +103,14 @@ export interface RuntimeTest = Record; + } + | undefined; +} + let unhandled_rejection: Error | null = null; function unhandled_rejection_handler(err: Error) { @@ -114,6 +124,10 @@ beforeAll(() => { process.prependListener('unhandledRejection', unhandled_rejection_handler); }); +beforeEach(() => { + delete globalThis?.__svelte?.h; +}); + afterAll(() => { process.removeListener('unhandledRejection', unhandled_rejection_handler); }); @@ -251,7 +265,16 @@ async function run_test_variant( i++; } - if (str.slice(0, i).includes('logs')) { + let ssr_str = config.test_ssr?.toString() ?? ''; + let sn = 0; + let si = 0; + while (si < ssr_str.length) { + if (ssr_str[si] === '(') sn++; + if (ssr_str[si] === ')' && --sn === 0) break; + si++; + } + + if (str.slice(0, i).includes('logs') || ssr_str.slice(0, si).includes('logs')) { // eslint-disable-next-line no-console console.log = (...args) => { logs.push(...args); @@ -262,7 +285,11 @@ async function run_test_variant( manual_hydrate = true; } - if (str.slice(0, i).includes('warnings') || config.warnings) { + if ( + str.slice(0, i).includes('warnings') || + config.warnings || + ssr_str.slice(0, si).includes('warnings') + ) { // eslint-disable-next-line no-console console.warn = (...args) => { if (typeof args[0] === 'string' && args[0].startsWith('%c[svelte]')) { @@ -382,6 +409,7 @@ async function run_test_variant( if (config.test_ssr) { await config.test_ssr({ logs, + warnings, // @ts-expect-error assert: { ...assert, @@ -402,6 +430,15 @@ async function run_test_variant( throw new Error('Ensure dom mode is skipped'); }; + const run_hydratables_init = () => { + if (variant !== 'hydrate') return; + const script = [...document.head.querySelectorAll('script').values()].find((script) => + script.textContent?.includes('window.__svelte ??= {}') + )?.textContent; + if (!script) return; + (0, eval)(script); + }; + if (runes) { props = proxy({ ...(config.props || {}) }); @@ -410,6 +447,7 @@ async function run_test_variant( if (manual_hydrate && variant === 'hydrate') { hydrate_fn = () => { + run_hydratables_init(); instance = hydrate(mod.default, { target, props, @@ -418,6 +456,7 @@ async function run_test_variant( }); }; } else { + run_hydratables_init(); const render = variant === 'hydrate' ? hydrate : mount; instance = render(mod.default, { target, @@ -427,6 +466,7 @@ async function run_test_variant( }); } } else { + run_hydratables_init(); instance = createClassComponent({ component: mod.default, props: config.props, @@ -533,6 +573,10 @@ async function run_test_variant( throw err; } } finally { + if (hydrating) { + throw new Error('Hydration state was not cleared'); + } + config.after_test?.(); // Free up the microtask queue diff --git a/packages/svelte/tests/runtime-runes/samples/async-action-blockers/_config.js b/packages/svelte/tests/runtime-runes/samples/async-action-blockers/_config.js new file mode 100644 index 0000000000..9d8aea3c17 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-action-blockers/_config.js @@ -0,0 +1,11 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + mode: ['client', 'hydrate'], + async test({ assert, logs }) { + await tick(); + + assert.deepEqual(logs, ['ready']); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-action-blockers/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-action-blockers/main.svelte new file mode 100644 index 0000000000..c9a1477a00 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-action-blockers/main.svelte @@ -0,0 +1,13 @@ + + +
    diff --git a/packages/svelte/tests/runtime-runes/samples/async-attach-blockers/Child.svelte b/packages/svelte/tests/runtime-runes/samples/async-attach-blockers/Child.svelte new file mode 100644 index 0000000000..346f9c4a19 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-attach-blockers/Child.svelte @@ -0,0 +1,5 @@ + + +
    \ No newline at end of file diff --git a/packages/svelte/tests/runtime-runes/samples/async-attach-blockers/_config.js b/packages/svelte/tests/runtime-runes/samples/async-attach-blockers/_config.js new file mode 100644 index 0000000000..4c83634e70 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-attach-blockers/_config.js @@ -0,0 +1,11 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + mode: ['client', 'hydrate'], + async test({ assert, logs }) { + await tick(); + + assert.deepEqual(logs, ['ready', 'ready']); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-attach-blockers/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-attach-blockers/main.svelte new file mode 100644 index 0000000000..30ba602350 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-attach-blockers/main.svelte @@ -0,0 +1,17 @@ + + +
    + diff --git a/packages/svelte/tests/runtime-runes/samples/async-await-block/_config.js b/packages/svelte/tests/runtime-runes/samples/async-await-block/_config.js new file mode 100644 index 0000000000..1f354a5175 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-await-block/_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, '

    1

    '); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-await-block/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-await-block/main.svelte new file mode 100644 index 0000000000..d746cd979c --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-await-block/main.svelte @@ -0,0 +1,7 @@ + + +{#await foo then x} +

    {x}

    +{/await} 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-batch-timing/Component.svelte b/packages/svelte/tests/runtime-runes/samples/async-batch-timing/Component.svelte new file mode 100644 index 0000000000..275860ad9c --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-batch-timing/Component.svelte @@ -0,0 +1,14 @@ + + +

    {ref_exists}

    + diff --git a/packages/svelte/tests/runtime-runes/samples/async-batch-timing/_config.js b/packages/svelte/tests/runtime-runes/samples/async-batch-timing/_config.js new file mode 100644 index 0000000000..1da4fdc0bd --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-batch-timing/_config.js @@ -0,0 +1,23 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +// This test regresses against batches deactivating other batches than themselves +export default test({ + async test({ assert, target }) { + await tick(); // settle initial await + + const button = target.querySelector('button'); + + button?.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` +
    div
    +

    true

    + +

    + ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-batch-timing/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-batch-timing/main.svelte new file mode 100644 index 0000000000..8b582f41d0 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-batch-timing/main.svelte @@ -0,0 +1,15 @@ + + +
    div
    + + + +{#if foo} +

    +{/if} diff --git a/packages/svelte/tests/runtime-runes/samples/async-bindable-prop/Child.svelte b/packages/svelte/tests/runtime-runes/samples/async-bindable-prop/Child.svelte new file mode 100644 index 0000000000..4214a85f37 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-bindable-prop/Child.svelte @@ -0,0 +1,8 @@ + + + +{value} + diff --git a/packages/svelte/tests/runtime-runes/samples/async-bindable-prop/_config.js b/packages/svelte/tests/runtime-runes/samples/async-bindable-prop/_config.js new file mode 100644 index 0000000000..a4ff70a8dc --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-bindable-prop/_config.js @@ -0,0 +1,35 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + mode: ['async-server', 'client', 'hydrate'], + ssrHtml: ` + initial

    initial

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

    initial

    + ` + ); + + const button = target.querySelector('button'); + button?.click(); + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` + + updated +

    updated

    + ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-bindable-prop/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-bindable-prop/main.svelte new file mode 100644 index 0000000000..e053fdda6a --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-bindable-prop/main.svelte @@ -0,0 +1,9 @@ + + + +

    {value}

    + diff --git a/packages/svelte/tests/runtime-runes/samples/async-binding-after-await/Child.svelte b/packages/svelte/tests/runtime-runes/samples/async-binding-after-await/Child.svelte new file mode 100644 index 0000000000..6cb93933ec --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-binding-after-await/Child.svelte @@ -0,0 +1,5 @@ + + +{value} diff --git a/packages/svelte/tests/runtime-runes/samples/async-binding-after-await/_config.js b/packages/svelte/tests/runtime-runes/samples/async-binding-after-await/_config.js new file mode 100644 index 0000000000..3068cace2d --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-binding-after-await/_config.js @@ -0,0 +1,14 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + mode: ['async-server', 'client', 'hydrate'], + ssrHtml: 'value value
    false
    ', + + async test({ assert, target, logs }) { + await tick(); + + assert.htmlEqual(target.innerHTML, 'value value
    true
    '); + assert.deepEqual(logs, [false, 'value', true, 'value']); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-binding-after-await/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-binding-after-await/main.svelte new file mode 100644 index 0000000000..9e4096581e --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-binding-after-await/main.svelte @@ -0,0 +1,17 @@ + + + + value, v => value = v} /> +
    {!!ref}
    + + value, v => value = v} /> \ No newline at end of file diff --git a/packages/svelte/tests/runtime-runes/samples/async-block-effect-queueing/A.svelte b/packages/svelte/tests/runtime-runes/samples/async-block-effect-queueing/A.svelte new file mode 100644 index 0000000000..7971deff5f --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-block-effect-queueing/A.svelte @@ -0,0 +1,15 @@ + + + diff --git a/packages/svelte/tests/runtime-runes/samples/async-block-effect-queueing/B.svelte b/packages/svelte/tests/runtime-runes/samples/async-block-effect-queueing/B.svelte new file mode 100644 index 0000000000..7371f47a6f --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-block-effect-queueing/B.svelte @@ -0,0 +1 @@ +B \ No newline at end of file diff --git a/packages/svelte/tests/runtime-runes/samples/async-block-effect-queueing/_config.js b/packages/svelte/tests/runtime-runes/samples/async-block-effect-queueing/_config.js new file mode 100644 index 0000000000..789cdfaa02 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-block-effect-queueing/_config.js @@ -0,0 +1,63 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + const [fork, commit, toggle] = target.querySelectorAll('button'); + + fork.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + + ` + ); + + toggle.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + + ` + ); + + toggle.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + + ` + ); + + toggle.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + + ` + ); + + commit.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + B + ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-block-effect-queueing/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-block-effect-queueing/main.svelte new file mode 100644 index 0000000000..7342a37f05 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-block-effect-queueing/main.svelte @@ -0,0 +1,24 @@ + + + + + + +{#if open} + +{:else} + +{/if} 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-component-exports/Child.svelte b/packages/svelte/tests/runtime-runes/samples/async-component-exports/Child.svelte new file mode 100644 index 0000000000..d5ad4754fb --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-component-exports/Child.svelte @@ -0,0 +1,9 @@ + diff --git a/packages/svelte/tests/runtime-runes/samples/async-component-exports/_config.js b/packages/svelte/tests/runtime-runes/samples/async-component-exports/_config.js new file mode 100644 index 0000000000..a4e4f24360 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-component-exports/_config.js @@ -0,0 +1,12 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target, logs }) { + await tick(); + const [log] = target.querySelectorAll('button'); + + log.click(); + assert.deepEqual(logs, ['foo', 'bar']); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-component-exports/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-component-exports/main.svelte new file mode 100644 index 0000000000..9e4d07ddfe --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-component-exports/main.svelte @@ -0,0 +1,11 @@ + + + + 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-const/_config.js b/packages/svelte/tests/runtime-runes/samples/async-const/_config.js index 8aeca875f3..0dd4b870d5 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-const/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-const/_config.js @@ -2,11 +2,12 @@ import { tick } from 'svelte'; import { test } from '../../test'; export default test({ - html: `

    Loading...

    `, + mode: ['async-server', 'client', 'hydrate'], + ssrHtml: `

    Hello, world!

    5 01234 5 sync 6 5 0 10`, async test({ assert, target }) { await tick(); - assert.htmlEqual(target.innerHTML, `

    Hello, world!

    5 01234`); + assert.htmlEqual(target.innerHTML, `

    Hello, world!

    5 01234 5 sync 6 5 0 10`); } }); diff --git a/packages/svelte/tests/runtime-runes/samples/async-const/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-const/main.svelte index 7410ff6a6f..4212c59fdc 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-const/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/async-const/main.svelte @@ -3,17 +3,16 @@ + {@const sync = 'sync'} {@const number = await Promise.resolve(5)} - - {#snippet pending()} -

    Loading...

    - {/snippet} + {@const after_async = number + 1} + {@const { length, 0: first } = await '01234'} {#snippet greet()} {@const greeting = await `Hello, ${name}!`}

    {greeting}

    {number} - {#if number > 4} + {#if number > 4 && after_async && greeting} {@const length = await number} {#each { length }, index} {@const i = await index} @@ -23,4 +22,10 @@ {/snippet} {@render greet()} + {number} {sync} {after_async} {length} {first} + + {#if sync} + {@const double = number * 2} + {double} + {/if}
    diff --git a/packages/svelte/tests/runtime-runes/samples/async-context-throws-after-await/_config.js b/packages/svelte/tests/runtime-runes/samples/async-context-throws-after-await/_config.js new file mode 100644 index 0000000000..be73968a88 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-context-throws-after-await/_config.js @@ -0,0 +1,11 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + mode: ['client'], + async test() { + // else runtime_error is checked too soon + await tick(); + }, + runtime_error: 'set_context_after_init' +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-context-throws-after-await/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-context-throws-after-await/main.svelte new file mode 100644 index 0000000000..8e770c214b --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-context-throws-after-await/main.svelte @@ -0,0 +1,7 @@ + diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-destructured/Child.svelte b/packages/svelte/tests/runtime-runes/samples/async-derived-destructured/Child.svelte index 39112b12a7..04adc8e97f 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-derived-destructured/Child.svelte +++ b/packages/svelte/tests/runtime-runes/samples/async-derived-destructured/Child.svelte @@ -1,13 +1,22 @@

    {count} ** 2 = {squared}

    {count} ** 3 = {cubed}

    +

    {typeof toFixed} {typeof toString}

    +

    {a} {b}

    \ No newline at end of file diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-destructured/_config.js b/packages/svelte/tests/runtime-runes/samples/async-derived-destructured/_config.js index d444e8e1d9..26f1dfdeb3 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-derived-destructured/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-derived-destructured/_config.js @@ -13,6 +13,8 @@ export default test({

    1 ** 2 = 1

    1 ** 3 = 1

    +

    function function

    +

    1 2

    ` ); @@ -25,6 +27,8 @@ export default test({

    2 ** 2 = 4

    2 ** 3 = 8

    +

    function function

    +

    1 2

    ` ); } diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-in-multiple-effects/Component.svelte b/packages/svelte/tests/runtime-runes/samples/async-derived-in-multiple-effects/Component.svelte new file mode 100644 index 0000000000..200778dc5b --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-derived-in-multiple-effects/Component.svelte @@ -0,0 +1,27 @@ + diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-in-multiple-effects/_config.js b/packages/svelte/tests/runtime-runes/samples/async-derived-in-multiple-effects/_config.js new file mode 100644 index 0000000000..15bb42074f --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-derived-in-multiple-effects/_config.js @@ -0,0 +1,16 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target, logs }) { + const button = target.querySelector('button'); + + button?.click(); + await tick(); + assert.deepEqual(logs, [5]); + + button?.click(); + await tick(); + assert.deepEqual(logs, [5, 7]); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-in-multiple-effects/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-derived-in-multiple-effects/main.svelte new file mode 100644 index 0000000000..bd82e35a3b --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-derived-in-multiple-effects/main.svelte @@ -0,0 +1,19 @@ + + + + {await new Promise((r) => { + // long enough for the test to do all its other stuff while this is pending + setTimeout(r, 10); + })} + {#snippet pending()}{/snippet} + + + + +{#if count > 0} + +{/if} diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-unowned/Component.svelte b/packages/svelte/tests/runtime-runes/samples/async-derived-unowned/Component.svelte new file mode 100644 index 0000000000..f7d138a3ed --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-derived-unowned/Component.svelte @@ -0,0 +1,6 @@ + + +

    {double}

    diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-unowned/_config.js b/packages/svelte/tests/runtime-runes/samples/async-derived-unowned/_config.js new file mode 100644 index 0000000000..fc0135623d --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-derived-unowned/_config.js @@ -0,0 +1,30 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + const button = target.querySelector('button'); + + button?.click(); + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` + +

    2

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

    4

    + ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-unowned/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-derived-unowned/main.svelte new file mode 100644 index 0000000000..bd82e35a3b --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-derived-unowned/main.svelte @@ -0,0 +1,19 @@ + + + + {await new Promise((r) => { + // long enough for the test to do all its other stuff while this is pending + setTimeout(r, 10); + })} + {#snippet pending()}{/snippet} + + + + +{#if count > 0} + +{/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-dynamic-component/Component.svelte b/packages/svelte/tests/runtime-runes/samples/async-dynamic-component/Component.svelte new file mode 100644 index 0000000000..40816a2b5a --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-dynamic-component/Component.svelte @@ -0,0 +1 @@ +Hi \ No newline at end of file diff --git a/packages/svelte/tests/runtime-runes/samples/async-dynamic-component/_config.js b/packages/svelte/tests/runtime-runes/samples/async-dynamic-component/_config.js new file mode 100644 index 0000000000..bf13a9b120 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-dynamic-component/_config.js @@ -0,0 +1,11 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + mode: ['async-server', 'client', 'hydrate'], + ssrHtml: 'Hi Hi Hi Hi', + async test({ assert, target }) { + await tick(); + assert.htmlEqual(target.innerHTML, 'Hi Hi Hi Hi'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-dynamic-component/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-dynamic-component/main.svelte new file mode 100644 index 0000000000..d959f80694 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-dynamic-component/main.svelte @@ -0,0 +1,10 @@ + + + + + + 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-derived/_config.js b/packages/svelte/tests/runtime-runes/samples/async-each-derived/_config.js new file mode 100644 index 0000000000..62a09bfc7c --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-each-derived/_config.js @@ -0,0 +1,20 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + await tick(); // settle initial await + + const checkBox = target.querySelector('input'); + + checkBox?.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + +

    true

    + ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-each-derived/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-each-derived/main.svelte new file mode 100644 index 0000000000..7e3b5c54bc --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-each-derived/main.svelte @@ -0,0 +1,11 @@ + + + + +{#each checked === foo && [1]} +

    {checked}

    +{/each} diff --git a/packages/svelte/tests/runtime-runes/samples/async-each-overlap/_config.js b/packages/svelte/tests/runtime-runes/samples/async-each-overlap/_config.js new file mode 100644 index 0000000000..d03f9ad09d --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-each-overlap/_config.js @@ -0,0 +1,102 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + skip: true, + async test({ assert, target }) { + const [add, shift] = target.querySelectorAll('button'); + + add.click(); + await tick(); + add.click(); + await tick(); + add.click(); + await tick(); + + // TODO pending count / number of pushes is off + + assert.htmlEqual( + target.innerHTML, + ` + + +

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

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

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

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

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

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

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

    +
    not keyed: +
    1
    +
    2
    +
    3
    +
    4
    +
    +
    keyed: +
    1
    +
    2
    +
    3
    +
    4
    +
    + ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-each-overlap/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-each-overlap/main.svelte new file mode 100644 index 0000000000..af9d395457 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-each-overlap/main.svelte @@ -0,0 +1,48 @@ + + + + + +

    + pending={$effect.pending()} + values.length={values.length} + values=[{values}] +

    + +
    + not keyed: + {#each values as v} +
    + {await push(v)} +
    + {/each} +
    +
    + keyed: + {#each values as v(v)} +
    + {await push(v)} +
    + {/each} +
    diff --git a/packages/svelte/tests/runtime-runes/samples/async-fork-attributes/_config.js b/packages/svelte/tests/runtime-runes/samples/async-fork-attributes/_config.js new file mode 100644 index 0000000000..59bcdeb7f5 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-fork-attributes/_config.js @@ -0,0 +1,60 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + const [fork, commit] = target.querySelectorAll('button'); + + fork.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + +

    foo

    +

    foo

    +

    foo

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

    foo

    +

    foo

    +

    foo

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

    foo

    +

    foo

    +

    foo

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

    foo

    +

    foo

    +

    foo

    + ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-fork-attributes/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-fork-attributes/main.svelte new file mode 100644 index 0000000000..956e5df6f3 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-fork-attributes/main.svelte @@ -0,0 +1,28 @@ + + + + + + + +

    foo

    +

    foo

    +

    foo

    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-if/Child.svelte b/packages/svelte/tests/runtime-runes/samples/async-fork-if/Child.svelte new file mode 100644 index 0000000000..6ef7d03eea --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-fork-if/Child.svelte @@ -0,0 +1,8 @@ + + +{x} diff --git a/packages/svelte/tests/runtime-runes/samples/async-fork-if/_config.js b/packages/svelte/tests/runtime-runes/samples/async-fork-if/_config.js new file mode 100644 index 0000000000..1bc168d9ae --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-fork-if/_config.js @@ -0,0 +1,12 @@ +import { test } from '../../test'; + +export default test({ + async test({ assert, target, logs }) { + const btn = target.querySelector('button'); + + btn?.click(); + await new Promise((r) => setTimeout(r, 2)); + assert.htmlEqual(target.innerHTML, ` universe`); + assert.deepEqual(logs, ['universe', 'universe']); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-fork-if/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-fork-if/main.svelte new file mode 100644 index 0000000000..625040ec13 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-fork-if/main.svelte @@ -0,0 +1,17 @@ + + + + +{#if x === 'universe'} + +{/if} 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-fork-update-same-state/_config.js b/packages/svelte/tests/runtime-runes/samples/async-fork-update-same-state/_config.js new file mode 100644 index 0000000000..b3e04204b4 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-fork-update-same-state/_config.js @@ -0,0 +1,49 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target, logs }) { + assert.deepEqual(logs, [0]); + + const [fork1, fork2, commit] = target.querySelectorAll('button'); + + fork1.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + +

    0

    + ` + ); + assert.deepEqual(logs, [0]); + + fork2.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + +

    0

    + ` + ); + assert.deepEqual(logs, [0]); + + commit.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + +

    1

    + ` + ); + assert.deepEqual(logs, [0, 1]); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-fork-update-same-state/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-fork-update-same-state/main.svelte new file mode 100644 index 0000000000..45645b4085 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-fork-update-same-state/main.svelte @@ -0,0 +1,37 @@ + + + + + + + + +

    {count}

    diff --git a/packages/svelte/tests/runtime-runes/samples/async-fork/_config.js b/packages/svelte/tests/runtime-runes/samples/async-fork/_config.js new file mode 100644 index 0000000000..35b47525a2 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-fork/_config.js @@ -0,0 +1,92 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target, raf }) { + const [shift, increment, commit] = target.querySelectorAll('button'); + + shift.click(); + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` + + + +

    count: 0

    +

    eager: 0

    +

    even

    + ` + ); + + increment.click(); + await tick(); + + shift.click(); + await tick(); + + // nothing updates until commit + assert.htmlEqual( + target.innerHTML, + ` + + + +

    count: 0

    +

    eager: 0

    +

    even

    + ` + ); + + commit.click(); + await tick(); + + // nothing updates until commit + assert.htmlEqual( + target.innerHTML, + ` + + + +

    count: 1

    +

    eager: 1

    +

    odd

    + ` + ); + + increment.click(); + await tick(); + + commit.click(); + await tick(); + + // eager state updates on commit + assert.htmlEqual( + target.innerHTML, + ` + + + +

    count: 1

    +

    eager: 2

    +

    odd

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

    count: 2

    +

    eager: 2

    +

    even

    + ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-fork/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-fork/main.svelte new file mode 100644 index 0000000000..9b1f433f2d --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-fork/main.svelte @@ -0,0 +1,37 @@ + + + + + + +

    count: {count}

    +

    eager: {$state.eager(count)}

    + + + {#if await push(count) % 2 === 0} +

    even

    + {:else} +

    odd

    + {/if} + + {#snippet pending()} +

    loading...

    + {/snippet} +
    diff --git a/packages/svelte/tests/runtime-runes/samples/async-head-title-1/Inner.svelte b/packages/svelte/tests/runtime-runes/samples/async-head-title-1/Inner.svelte new file mode 100644 index 0000000000..089ba43607 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-head-title-1/Inner.svelte @@ -0,0 +1,15 @@ + + + + title + + +

    {await push()}

    diff --git a/packages/svelte/tests/runtime-runes/samples/async-head-title-1/_config.js b/packages/svelte/tests/runtime-runes/samples/async-head-title-1/_config.js new file mode 100644 index 0000000000..39cbf5becb --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-head-title-1/_config.js @@ -0,0 +1,24 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + const [toggle, resolve] = target.querySelectorAll('button'); + toggle.click(); + await tick(); + assert.equal(window.document.title, ''); + + toggle.click(); + await tick(); + assert.equal(window.document.title, ''); + + toggle.click(); + await tick(); + assert.equal(window.document.title, ''); + + resolve.click(); + await tick(); + await tick(); + assert.equal(window.document.title, 'title'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-head-title-1/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-head-title-1/main.svelte new file mode 100644 index 0000000000..3535157087 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-head-title-1/main.svelte @@ -0,0 +1,12 @@ + + + + +{#if show} + +{/if} diff --git a/packages/svelte/tests/runtime-runes/samples/async-head-title-2/Inner.svelte b/packages/svelte/tests/runtime-runes/samples/async-head-title-2/Inner.svelte new file mode 100644 index 0000000000..b2a8656276 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-head-title-2/Inner.svelte @@ -0,0 +1,13 @@ + + + + {await push()} + diff --git a/packages/svelte/tests/runtime-runes/samples/async-head-title-2/_config.js b/packages/svelte/tests/runtime-runes/samples/async-head-title-2/_config.js new file mode 100644 index 0000000000..b89dce62d1 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-head-title-2/_config.js @@ -0,0 +1,23 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + const [toggle, resolve] = target.querySelectorAll('button'); + toggle.click(); + await tick(); + assert.equal(window.document.title, ''); + + toggle.click(); + await tick(); + assert.equal(window.document.title, ''); + + toggle.click(); + await tick(); + assert.equal(window.document.title, ''); + + resolve.click(); + await tick(); + assert.equal(window.document.title, 'title'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-head-title-2/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-head-title-2/main.svelte new file mode 100644 index 0000000000..3535157087 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-head-title-2/main.svelte @@ -0,0 +1,12 @@ + + + + +{#if show} + +{/if} diff --git a/packages/svelte/tests/runtime-runes/samples/async-head-title-3/Inner.svelte b/packages/svelte/tests/runtime-runes/samples/async-head-title-3/Inner.svelte new file mode 100644 index 0000000000..4d761c92ef --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-head-title-3/Inner.svelte @@ -0,0 +1,15 @@ + + + + {title} + + +

    {await push()}

    diff --git a/packages/svelte/tests/runtime-runes/samples/async-head-title-3/_config.js b/packages/svelte/tests/runtime-runes/samples/async-head-title-3/_config.js new file mode 100644 index 0000000000..39cbf5becb --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-head-title-3/_config.js @@ -0,0 +1,24 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + const [toggle, resolve] = target.querySelectorAll('button'); + toggle.click(); + await tick(); + assert.equal(window.document.title, ''); + + toggle.click(); + await tick(); + assert.equal(window.document.title, ''); + + toggle.click(); + await tick(); + assert.equal(window.document.title, ''); + + resolve.click(); + await tick(); + await tick(); + assert.equal(window.document.title, 'title'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-head-title-3/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-head-title-3/main.svelte new file mode 100644 index 0000000000..be4f04afe8 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-head-title-3/main.svelte @@ -0,0 +1,12 @@ + + + + +{#if show} + +{/if} diff --git a/packages/svelte/tests/runtime-runes/samples/async-head/A.svelte b/packages/svelte/tests/runtime-runes/samples/async-head/A.svelte new file mode 100644 index 0000000000..d821bb6fa0 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-head/A.svelte @@ -0,0 +1,7 @@ + + + + + diff --git a/packages/svelte/tests/runtime-runes/samples/async-head/B.svelte b/packages/svelte/tests/runtime-runes/samples/async-head/B.svelte new file mode 100644 index 0000000000..d725d5f03b --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-head/B.svelte @@ -0,0 +1,8 @@ + + + + + + diff --git a/packages/svelte/tests/runtime-runes/samples/async-head/_config.js b/packages/svelte/tests/runtime-runes/samples/async-head/_config.js new file mode 100644 index 0000000000..6fdf41b434 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-head/_config.js @@ -0,0 +1,23 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, window }) { + await tick(); + + const head = window.document.head; + + // we don't care about the order, but we want to ensure that the + // elements didn't clobber each other + for (let n of ['1', '2', '3']) { + const a = head.querySelector(`meta[name="a-${n}"]`); + assert.equal(a?.getAttribute('content'), n); + + const b1 = head.querySelector(`meta[name="b-${n}-1"]`); + assert.equal(b1?.getAttribute('content'), `${n}-1`); + + const b2 = head.querySelector(`meta[name="b-${n}-2"]`); + assert.equal(b2?.getAttribute('content'), `${n}-2`); + } + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-head/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-head/main.svelte new file mode 100644 index 0000000000..7f23489373 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-head/main.svelte @@ -0,0 +1,11 @@ + + +
    + + + + + diff --git a/packages/svelte/tests/runtime-runes/samples/async-if-after-await-in-script/_config.js b/packages/svelte/tests/runtime-runes/samples/async-if-after-await-in-script/_config.js new file mode 100644 index 0000000000..2b8ab6e894 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-if-after-await-in-script/_config.js @@ -0,0 +1,16 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + skip_mode: ['server'], + + ssrHtml: '

    yep

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

    yep

    '); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-if-after-await-in-script/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-if-after-await-in-script/main.svelte new file mode 100644 index 0000000000..66dc7c718c --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-if-after-await-in-script/main.svelte @@ -0,0 +1,10 @@ + + +{#if condition} +

    yep

    +{:else} +

    nope

    +{/if} diff --git a/packages/svelte/tests/runtime-runes/samples/async-indirect-blockers/Component1.svelte b/packages/svelte/tests/runtime-runes/samples/async-indirect-blockers/Component1.svelte new file mode 100644 index 0000000000..a3bb9d92b3 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-indirect-blockers/Component1.svelte @@ -0,0 +1,13 @@ + + + +

    {getValue()}

    diff --git a/packages/svelte/tests/runtime-runes/samples/async-indirect-blockers/Component2.svelte b/packages/svelte/tests/runtime-runes/samples/async-indirect-blockers/Component2.svelte new file mode 100644 index 0000000000..8b28cf5708 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-indirect-blockers/Component2.svelte @@ -0,0 +1,11 @@ + + + +

    {getValue()}

    diff --git a/packages/svelte/tests/runtime-runes/samples/async-indirect-blockers/Component3.svelte b/packages/svelte/tests/runtime-runes/samples/async-indirect-blockers/Component3.svelte new file mode 100644 index 0000000000..f26daeb4f2 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-indirect-blockers/Component3.svelte @@ -0,0 +1,16 @@ + + + +

    {getValue()}

    diff --git a/packages/svelte/tests/runtime-runes/samples/async-indirect-blockers/Component4.svelte b/packages/svelte/tests/runtime-runes/samples/async-indirect-blockers/Component4.svelte new file mode 100644 index 0000000000..564cb2660a --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-indirect-blockers/Component4.svelte @@ -0,0 +1,19 @@ + + + +

    {getValue()}

    diff --git a/packages/svelte/tests/runtime-runes/samples/async-indirect-blockers/_config.js b/packages/svelte/tests/runtime-runes/samples/async-indirect-blockers/_config.js new file mode 100644 index 0000000000..7b7ee5b122 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-indirect-blockers/_config.js @@ -0,0 +1,27 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + mode: ['async-server', 'client', 'hydrate'], + ssrHtml: + '

    ', + + async test({ assert, target }) { + await tick(); + + const inputs = Array.from(target.querySelectorAll('input')); + const paragraphs = Array.from(target.querySelectorAll('p')); + + for (let i = 0; i < 4; i++) { + assert.equal(inputs[i].value, ''); + assert.htmlEqual(paragraphs[i].innerHTML, ''); + + inputs[i].value = 'hello'; + inputs[i].dispatchEvent(new InputEvent('input', { bubbles: true })); + await tick(); + + assert.equal(inputs[i].value, 'hello'); + assert.htmlEqual(paragraphs[i].innerHTML, 'hello'); + } + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-indirect-blockers/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-indirect-blockers/main.svelte new file mode 100644 index 0000000000..c30111fd2b --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-indirect-blockers/main.svelte @@ -0,0 +1,11 @@ + + + + + + diff --git a/packages/svelte/tests/runtime-runes/samples/async-props-id/_config.js b/packages/svelte/tests/runtime-runes/samples/async-props-id/_config.js new file mode 100644 index 0000000000..3c6a6ed247 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-props-id/_config.js @@ -0,0 +1,11 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + ssrHtml: `

    s1

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

    s1

    ' : '

    c1

    '); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-props-id/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-props-id/main.svelte new file mode 100644 index 0000000000..973d6855a6 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-props-id/main.svelte @@ -0,0 +1,6 @@ + + +

    {id}

    diff --git a/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss-for-await/_config.js b/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss-for-await/_config.js index bde65a499f..ce7cd6bd49 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss-for-await/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss-for-await/_config.js @@ -2,6 +2,9 @@ import { tick } from 'svelte'; import { test } from '../../test'; export default test({ + // TODO reinstate + skip: true, + compileOptions: { dev: true }, @@ -17,7 +20,7 @@ export default test({ 'Detected reactivity loss when reading `values[1]`. This happens when state is read in an async function after an earlier `await`' ); - assert.equal(warnings[1].name, 'TracedAtError'); + assert.equal(warnings[1].name, 'traced at'); assert.equal(warnings.length, 2); } diff --git a/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss/_config.js b/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss/_config.js index 16318a3b44..ad333a573a 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss/_config.js @@ -2,6 +2,9 @@ import { tick } from 'svelte'; import { test } from '../../test'; export default test({ + // TODO reinstate this + skip: true, + compileOptions: { dev: true }, @@ -20,7 +23,7 @@ export default test({ 'Detected reactivity loss when reading `b`. This happens when state is read in an async function after an earlier `await`' ); - assert.equal(warnings[1].name, 'TracedAtError'); + assert.equal(warnings[1].name, 'traced at'); assert.equal(warnings.length, 2); } 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/C.svelte b/packages/svelte/tests/runtime-runes/samples/async-set-context/C.svelte new file mode 100644 index 0000000000..2aebf7a64f --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-set-context/C.svelte @@ -0,0 +1,8 @@ + + +

    {greeting} {recipient}

    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 new file mode 100644 index 0000000000..aebed211cc --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-set-context/_config.js @@ -0,0 +1,11 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + mode: ['client', 'async-server'], + ssrHtml: `

    hello world

    `, + async test({ assert, target }) { + await tick(); + 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 new file mode 100644 index 0000000000..e2189e7eb8 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-set-context/main.svelte @@ -0,0 +1,7 @@ + + +
    diff --git a/packages/svelte/tests/runtime-runes/samples/async-settled-after-dom/_config.js b/packages/svelte/tests/runtime-runes/samples/async-settled-after-dom/_config.js new file mode 100644 index 0000000000..2c9816f3ed --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-settled-after-dom/_config.js @@ -0,0 +1,27 @@ +import { settled, tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + mode: ['client'], + + async test({ assert, target }) { + const [shift, update] = target.querySelectorAll('button'); + + shift.click(); + await tick(); + + assert.htmlEqual(target.innerHTML, '

    hello

    '); + + update.click(); + const promise = settled(); + + await tick(); + shift.click(); + await promise; + + assert.htmlEqual( + target.innerHTML, + '

    goodbye

    ' + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-settled-after-dom/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-settled-after-dom/main.svelte new file mode 100644 index 0000000000..0db9f80118 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-settled-after-dom/main.svelte @@ -0,0 +1,20 @@ + + + + + + +

    {await push(text)}

    + + {#snippet pending()}{/snippet} +
    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-snippet-coordinated-mount/Child.svelte b/packages/svelte/tests/runtime-runes/samples/async-snippet-coordinated-mount/Child.svelte new file mode 100644 index 0000000000..7085219a5a --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-snippet-coordinated-mount/Child.svelte @@ -0,0 +1,7 @@ + + +

    message: {message}

    +{@render children()} diff --git a/packages/svelte/tests/runtime-runes/samples/async-snippet-coordinated-mount/_config.js b/packages/svelte/tests/runtime-runes/samples/async-snippet-coordinated-mount/_config.js new file mode 100644 index 0000000000..b6ca2ae3d2 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-snippet-coordinated-mount/_config.js @@ -0,0 +1,25 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + const [shift] = target.querySelectorAll('button'); + + shift.click(); + await tick(); + + assert.htmlEqual(target.innerHTML, `

    loading...

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

    message: hello from child

    +

    hello from parent

    + ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-snippet-coordinated-mount/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-snippet-coordinated-mount/main.svelte new file mode 100644 index 0000000000..3ad2c9572a --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-snippet-coordinated-mount/main.svelte @@ -0,0 +1,21 @@ + + + + + + +

    {await push('hello from parent')}

    +
    + + {#snippet pending()} +

    loading...

    + {/snippet} +
    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-static-prop-after-await/Child.svelte b/packages/svelte/tests/runtime-runes/samples/async-static-prop-after-await/Child.svelte new file mode 100644 index 0000000000..e93bbe3dc0 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-static-prop-after-await/Child.svelte @@ -0,0 +1,5 @@ + + +{value} diff --git a/packages/svelte/tests/runtime-runes/samples/async-static-prop-after-await/_config.js b/packages/svelte/tests/runtime-runes/samples/async-static-prop-after-await/_config.js new file mode 100644 index 0000000000..eff988e0d6 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-static-prop-after-await/_config.js @@ -0,0 +1,11 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + mode: ['async-server', 'client', 'hydrate'], + ssrHtml: 'value
    ', + async test({ assert, target }) { + await tick(); + assert.htmlEqual(target.innerHTML, 'value
    '); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-static-prop-after-await/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-static-prop-after-await/main.svelte new file mode 100644 index 0000000000..1083882e1f --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-static-prop-after-await/main.svelte @@ -0,0 +1,10 @@ + + + +
    diff --git a/packages/svelte/tests/runtime-runes/samples/async-style-after-await/_config.js b/packages/svelte/tests/runtime-runes/samples/async-style-after-await/_config.js new file mode 100644 index 0000000000..472e7076c3 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-style-after-await/_config.js @@ -0,0 +1,30 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + mode: ['async-server', 'client', 'hydrate'], + ssrHtml: ` +
    +
    + +
    +
    + + `, + + async test({ assert, target }) { + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` +
    +
    + +
    +
    + + ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-style-after-await/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-style-after-await/main.svelte new file mode 100644 index 0000000000..7c1abeda2c --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-style-after-await/main.svelte @@ -0,0 +1,25 @@ + + + + + +{#if color} +
    +
    + +{/if} + + +{#if color} +
    +
    + +{/if} 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/async-transition-blockers/_config.js b/packages/svelte/tests/runtime-runes/samples/async-transition-blockers/_config.js new file mode 100644 index 0000000000..9d8aea3c17 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-transition-blockers/_config.js @@ -0,0 +1,11 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + mode: ['client', 'hydrate'], + async test({ assert, logs }) { + await tick(); + + assert.deepEqual(logs, ['ready']); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-transition-blockers/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-transition-blockers/main.svelte new file mode 100644 index 0000000000..ff5059e129 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-transition-blockers/main.svelte @@ -0,0 +1,13 @@ + + +
    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/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/derived-rest-includes-symbol/_config.js b/packages/svelte/tests/runtime-runes/samples/derived-rest-includes-symbol/_config.js new file mode 100644 index 0000000000..d0633983d2 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/derived-rest-includes-symbol/_config.js @@ -0,0 +1,5 @@ +import { test } from '../../test'; + +export default test({ + html: `

    true false

    ` +}); diff --git a/packages/svelte/tests/runtime-runes/samples/derived-rest-includes-symbol/main.svelte b/packages/svelte/tests/runtime-runes/samples/derived-rest-includes-symbol/main.svelte new file mode 100644 index 0000000000..2454e98ab7 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/derived-rest-includes-symbol/main.svelte @@ -0,0 +1,11 @@ + + +

    {symbol1 in b} {symbol2 in b}

    diff --git a/packages/svelte/tests/runtime-runes/samples/each-effect-linking/_config.js b/packages/svelte/tests/runtime-runes/samples/each-effect-linking/_config.js new file mode 100644 index 0000000000..d1c23527a9 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/each-effect-linking/_config.js @@ -0,0 +1,32 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + const [step_back, step_forward, jump_back, jump_forward] = target.querySelectorAll('button'); + const [div] = target.querySelectorAll('div'); + + step_back.click(); + await tick(); + + step_forward.click(); + await tick(); + + step_forward.click(); + await tick(); + + // if the effects get linked in a circle, we will never get here + assert.htmlEqual(div.innerHTML, '

    5

    6

    7

    '); + + jump_forward.click(); + await tick(); + + step_forward.click(); + await tick(); + + step_forward.click(); + await tick(); + + assert.htmlEqual(div.innerHTML, '

    12

    13

    14

    '); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/each-effect-linking/main.svelte b/packages/svelte/tests/runtime-runes/samples/each-effect-linking/main.svelte new file mode 100644 index 0000000000..ea33b11e22 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/each-effect-linking/main.svelte @@ -0,0 +1,33 @@ + + + + + + + + + + +
    + {#each items as item (item)} +

    {item}

    + {/each} +
    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/each-updates-10/_config.js b/packages/svelte/tests/runtime-runes/samples/each-updates-10/_config.js new file mode 100644 index 0000000000..d5c9e36f1d --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/each-updates-10/_config.js @@ -0,0 +1,51 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + const [add, adjust] = target.querySelectorAll('button'); + + add.click(); + flushSync(); + assert.htmlEqual( + target.innerHTML, + ` +

    Keyed

    +
    Item: 1. Index: 0
    +
    Item: 0. Index: 1
    +

    Unkeyed

    +
    Item: 1. Index: 0
    +
    Item: 0. Index: 1
    ` + ); + + add.click(); + flushSync(); + assert.htmlEqual( + target.innerHTML, + ` +

    Keyed

    +
    Item: 2. Index: 0
    +
    Item: 1. Index: 1
    +
    Item: 0. Index: 2
    +

    Unkeyed

    +
    Item: 2. Index: 0
    +
    Item: 1. Index: 1
    +
    Item: 0. Index: 2
    ` + ); + + adjust.click(); + flushSync(); + assert.htmlEqual( + target.innerHTML, + ` +

    Keyed

    +
    Item: 2. Index: 0
    +
    Item: 1. Index: 1
    +
    Item: 10. Index: 2
    +

    Unkeyed

    +
    Item: 2. Index: 0
    +
    Item: 1. Index: 1
    +
    Item: 10. Index: 2
    ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/each-updates-10/main.svelte b/packages/svelte/tests/runtime-runes/samples/each-updates-10/main.svelte new file mode 100644 index 0000000000..20ce8279de --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/each-updates-10/main.svelte @@ -0,0 +1,16 @@ + + + + + +

    Keyed

    +{#each items as item, index (item)} +
    Item: {item.t}. Index: {index}
    +{/each} + +

    Unkeyed

    +{#each items as item, index} +
    Item: {item.t}. Index: {index}
    +{/each} diff --git a/packages/svelte/tests/runtime-runes/samples/each-updates-11/_config.js b/packages/svelte/tests/runtime-runes/samples/each-updates-11/_config.js new file mode 100644 index 0000000000..a8782d2da8 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/each-updates-11/_config.js @@ -0,0 +1,32 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + const [add4, add5, modify3] = target.querySelectorAll('button'); + + add4.click(); + flushSync(); + assert.htmlEqual( + target.innerHTML, + ` + 1423` + ); + + add5.click(); + flushSync(); + assert.htmlEqual( + target.innerHTML, + ` + 14523` + ); + + modify3.click(); + flushSync(); + assert.htmlEqual( + target.innerHTML, + ` + 1452updated` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/each-updates-11/main.svelte b/packages/svelte/tests/runtime-runes/samples/each-updates-11/main.svelte new file mode 100644 index 0000000000..1dcd265093 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/each-updates-11/main.svelte @@ -0,0 +1,11 @@ + + + + + + +{#each list as item (item.id)} + {item.text} +{/each} diff --git a/packages/svelte/tests/runtime-runes/samples/each-updates-12/_config.js b/packages/svelte/tests/runtime-runes/samples/each-updates-12/_config.js new file mode 100644 index 0000000000..1fee8ceb67 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/each-updates-12/_config.js @@ -0,0 +1,33 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target, raf }) { + const [clear, push] = target.querySelectorAll('button'); + + flushSync(() => clear.click()); + flushSync(() => push.click()); + raf.tick(500); + + assert.htmlEqual( + target.innerHTML, + ` + + + 1 + 2 + ` + ); + + raf.tick(1000); + + assert.htmlEqual( + target.innerHTML, + ` + + + 1 + ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/each-updates-12/main.svelte b/packages/svelte/tests/runtime-runes/samples/each-updates-12/main.svelte new file mode 100644 index 0000000000..a65ebd37a8 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/each-updates-12/main.svelte @@ -0,0 +1,19 @@ + + + + + +{#each items as item} + {item} +{/each} diff --git a/packages/svelte/tests/runtime-runes/samples/each-updates-13/_config.js b/packages/svelte/tests/runtime-runes/samples/each-updates-13/_config.js new file mode 100644 index 0000000000..fdf02e486c --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/each-updates-13/_config.js @@ -0,0 +1,23 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target, raf }) { + const [clear, reverse] = target.querySelectorAll('button'); + + flushSync(() => clear.click()); + flushSync(() => reverse.click()); + raf.tick(1); + + assert.htmlEqual( + target.innerHTML, + ` + + + c + b + a + ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/each-updates-13/main.svelte b/packages/svelte/tests/runtime-runes/samples/each-updates-13/main.svelte new file mode 100644 index 0000000000..3de3382419 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/each-updates-13/main.svelte @@ -0,0 +1,19 @@ + + + + + +{#each items as item (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 400495050c..57f60c2b44 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 @@ -14,7 +14,7 @@ export default test({ try { flushSync(() => button.click()); } catch (e) { - assert.equal(errors.length, 1); // for whatever reason we can't get the name which should be UpdatedAtError + 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')); } } diff --git a/packages/svelte/tests/runtime-runes/samples/error-boundary-23/_config.js b/packages/svelte/tests/runtime-runes/samples/error-boundary-23/_config.js new file mode 100644 index 0000000000..7a6a66eb66 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/error-boundary-23/_config.js @@ -0,0 +1,12 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target, logs }) { + const btn = target.querySelector('button'); + btn?.click(); + await tick(); + + assert.deepEqual(logs, ['attachment']); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/error-boundary-23/main.svelte b/packages/svelte/tests/runtime-runes/samples/error-boundary-23/main.svelte new file mode 100644 index 0000000000..c1fe20d931 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/error-boundary-23/main.svelte @@ -0,0 +1,20 @@ + + + + {fail ? error() : 'all good'} + + + {#snippet failed()} +
    oops!
    + {/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/flush-sync-inside-attachment/_config.js b/packages/svelte/tests/runtime-runes/samples/flush-sync-inside-attachment/_config.js index ec8858b2c6..b34a90e901 100644 --- a/packages/svelte/tests/runtime-runes/samples/flush-sync-inside-attachment/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/flush-sync-inside-attachment/_config.js @@ -1,12 +1,8 @@ -import { async_mode } from '../../../helpers'; import { test } from '../../test'; export default test({ - // In legacy mode this succeeds and logs 'hello' - // In async mode this throws an error because flushSync is called inside an effect async test({ assert, target, logs }) { assert.htmlEqual(target.innerHTML, `
    hello
    `); assert.deepEqual(logs, ['hello']); - }, - runtime_error: async_mode ? 'flush_sync_in_effect' : undefined + } }); 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-value-immediate/_config.js b/packages/svelte/tests/runtime-runes/samples/fork-derived-value-immediate/_config.js new file mode 100644 index 0000000000..4f7ff673d6 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/fork-derived-value-immediate/_config.js @@ -0,0 +1,13 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + skip_no_async: true, + async test({ assert, target, logs }) { + const fork = target.querySelector('button'); + + fork?.click(); + flushSync(); + assert.deepEqual(logs, [1, 2]); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/fork-derived-value-immediate/main.svelte b/packages/svelte/tests/runtime-runes/samples/fork-derived-value-immediate/main.svelte new file mode 100644 index 0000000000..2adb83b735 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/fork-derived-value-immediate/main.svelte @@ -0,0 +1,15 @@ + + + diff --git a/packages/svelte/tests/runtime-runes/samples/fork-derived-value/_config.js b/packages/svelte/tests/runtime-runes/samples/fork-derived-value/_config.js new file mode 100644 index 0000000000..0635db7501 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/fork-derived-value/_config.js @@ -0,0 +1,20 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + skip_no_async: true, + async test({ assert, target }) { + const [fork, update] = target.querySelectorAll('button'); + + flushSync(() => { + fork.click(); + }); + flushSync(() => { + update.click(); + }); + + const p = target.querySelector('p'); + + assert.equal(p?.textContent, 'one'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/fork-derived-value/main.svelte b/packages/svelte/tests/runtime-runes/samples/fork-derived-value/main.svelte new file mode 100644 index 0000000000..06e0f1f264 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/fork-derived-value/main.svelte @@ -0,0 +1,20 @@ + + + + + + +{#if count === 1} +

    one

    +{/if} diff --git a/packages/svelte/tests/runtime-runes/samples/global-variable-assignment/_config.js b/packages/svelte/tests/runtime-runes/samples/global-variable-assignment/_config.js new file mode 100644 index 0000000000..37f4b2814c --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/global-variable-assignment/_config.js @@ -0,0 +1,6 @@ +import { test } from '../../test'; + +export default test({ + error: 'x is not defined', + async test() {} +}); diff --git a/packages/svelte/tests/runtime-runes/samples/global-variable-assignment/foo.svelte.js b/packages/svelte/tests/runtime-runes/samples/global-variable-assignment/foo.svelte.js new file mode 100644 index 0000000000..198b8f89e7 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/global-variable-assignment/foo.svelte.js @@ -0,0 +1 @@ +x = 1; diff --git a/packages/svelte/tests/runtime-runes/samples/global-variable-assignment/main.svelte b/packages/svelte/tests/runtime-runes/samples/global-variable-assignment/main.svelte new file mode 100644 index 0000000000..0ac6956b1d --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/global-variable-assignment/main.svelte @@ -0,0 +1,3 @@ + diff --git a/packages/svelte/tests/runtime-runes/samples/hydratable-complex-nesting/_config.js b/packages/svelte/tests/runtime-runes/samples/hydratable-complex-nesting/_config.js new file mode 100644 index 0000000000..0ac5333c4a --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/hydratable-complex-nesting/_config.js @@ -0,0 +1,24 @@ +import { tick } from 'svelte'; +import { ok, test } from '../../test'; + +export default test({ + skip_no_async: true, + skip_mode: ['server'], + + server_props: { environment: 'server' }, + ssrHtml: '

    The current environment is: server

    ', + + props: { environment: 'browser' }, + + async test({ assert, target, variant }) { + // make sure hydration has a chance to finish + await tick(); + const p = target.querySelector('p'); + ok(p); + if (variant === 'hydrate') { + assert.htmlEqual(p.outerHTML, '

    The current environment is: server

    '); + } else { + assert.htmlEqual(p.outerHTML, '

    The current environment is: browser

    '); + } + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/hydratable-complex-nesting/main.svelte b/packages/svelte/tests/runtime-runes/samples/hydratable-complex-nesting/main.svelte new file mode 100644 index 0000000000..cd603a6e6b --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/hydratable-complex-nesting/main.svelte @@ -0,0 +1,13 @@ + + +

    The current environment is: {await value.then(res => res.nested).then(res => res.environment)}

    diff --git a/packages/svelte/tests/runtime-runes/samples/hydratable-error-on-missing/_config.js b/packages/svelte/tests/runtime-runes/samples/hydratable-error-on-missing/_config.js new file mode 100644 index 0000000000..3349cbcb66 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/hydratable-error-on-missing/_config.js @@ -0,0 +1,13 @@ +import { ok, test } from '../../test'; + +export default test({ + skip_no_async: true, + mode: ['async-server', 'hydrate'], + + server_props: { environment: 'server' }, + ssrHtml: '

    The current environment is: server

    ', + + props: { environment: 'browser' }, + + runtime_error: 'hydratable_missing_but_required' +}); diff --git a/packages/svelte/tests/runtime-runes/samples/hydratable-error-on-missing/main.svelte b/packages/svelte/tests/runtime-runes/samples/hydratable-error-on-missing/main.svelte new file mode 100644 index 0000000000..b7dfc0e7e2 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/hydratable-error-on-missing/main.svelte @@ -0,0 +1,14 @@ + + +

    The current environment is: {value}

    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/hydratable-unused-keys-nesting-partial/_config.js b/packages/svelte/tests/runtime-runes/samples/hydratable-unused-keys-nesting-partial/_config.js new file mode 100644 index 0000000000..b1973a23c2 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/hydratable-unused-keys-nesting-partial/_config.js @@ -0,0 +1,27 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + skip_no_async: true, + mode: ['async-server', 'hydrate'], + + server_props: { environment: 'server' }, + ssrHtml: + '
    did you ever hear the tragedy of darth plagueis the wise?
    Loading...
    ', + + test_ssr({ assert, warnings }) { + assert.strictEqual(warnings.length, 1); + // for some strange reason we trim the error code off the beginning of warnings so I can't actually assert it + assert.include(warnings[0], 'A `hydratable` value with key `partially_used`'); + }, + + async test({ assert, target }) { + // make sure the hydratable promise on the client has a chance to run and reject (it shouldn't, because the server data should be used) + await tick(); + + assert.htmlEqual( + target.innerHTML, + '
    did you ever hear the tragedy of darth plagueis the wise?
    no, sith daddy, please tell me
    ' + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/hydratable-unused-keys-nesting-partial/main.svelte b/packages/svelte/tests/runtime-runes/samples/hydratable-unused-keys-nesting-partial/main.svelte new file mode 100644 index 0000000000..75723848b1 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/hydratable-unused-keys-nesting-partial/main.svelte @@ -0,0 +1,27 @@ + + +
    {await partially_used_hydratable.used}
    + +
    {await partially_used_hydratable.unused}
    + {#snippet pending()} +
    Loading...
    + {/snippet} +
    diff --git a/packages/svelte/tests/runtime-runes/samples/hydratable-unused-keys/_config.js b/packages/svelte/tests/runtime-runes/samples/hydratable-unused-keys/_config.js new file mode 100644 index 0000000000..44978c8d58 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/hydratable-unused-keys/_config.js @@ -0,0 +1,26 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + skip_no_async: true, + mode: ['async-server', 'hydrate'], + + server_props: { environment: 'server' }, + ssrHtml: '
    Loading...
    ', + + test_ssr({ assert, warnings }) { + assert.strictEqual(warnings.length, 1); + // for some strange reason we trim the error code off the beginning of warnings so I can't actually assert it + assert.include(warnings[0], 'A `hydratable` value with key `unused_key`'); + }, + + async test({ assert, target }) { + // make sure the hydratable promise on the client has a chance to run and reject (it shouldn't, because the server data should be used) + await tick(); + + assert.htmlEqual( + target.innerHTML, + '
    did you ever hear the tragedy of darth plagueis the wise?
    ' + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/hydratable-unused-keys/main.svelte b/packages/svelte/tests/runtime-runes/samples/hydratable-unused-keys/main.svelte new file mode 100644 index 0000000000..67848e7f6f --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/hydratable-unused-keys/main.svelte @@ -0,0 +1,19 @@ + + + +
    {await unresolved_hydratable}
    + {#snippet pending()} +
    Loading...
    + {/snippet} +
    diff --git a/packages/svelte/tests/runtime-runes/samples/hydratable/_config.js b/packages/svelte/tests/runtime-runes/samples/hydratable/_config.js new file mode 100644 index 0000000000..57904ef576 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/hydratable/_config.js @@ -0,0 +1,21 @@ +import { ok, test } from '../../test'; + +export default test({ + skip_no_async: true, + skip_mode: ['server'], + + server_props: { environment: 'server' }, + ssrHtml: '

    The current environment is: server

    ', + + props: { environment: 'browser' }, + + async test({ assert, target, variant }) { + const p = target.querySelector('p'); + ok(p); + if (variant === 'hydrate') { + assert.htmlEqual(p.outerHTML, '

    The current environment is: server

    '); + } else { + assert.htmlEqual(p.outerHTML, '

    The current environment is: browser

    '); + } + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/hydratable/main.svelte b/packages/svelte/tests/runtime-runes/samples/hydratable/main.svelte new file mode 100644 index 0000000000..53b9c24f91 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/hydratable/main.svelte @@ -0,0 +1,9 @@ + + +

    The current environment is: {value}

    diff --git a/packages/svelte/tests/runtime-runes/samples/inspect-derived-4/_config.js b/packages/svelte/tests/runtime-runes/samples/inspect-derived-4/_config.js new file mode 100644 index 0000000000..3a3bca7221 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/inspect-derived-4/_config.js @@ -0,0 +1,35 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; +import { normalise_inspect_logs } from '../../../helpers'; + +export default test({ + compileOptions: { + dev: true + }, + + async test({ assert, target, logs }) { + const [b] = target.querySelectorAll('button'); + + b.click(); + await tick(); + assert.htmlEqual(target.innerHTML, ``); + + b.click(); + await tick(); + assert.htmlEqual(target.innerHTML, ``); + + b.click(); + await tick(); + assert.htmlEqual(target.innerHTML, ``); + + assert.deepEqual(normalise_inspect_logs(logs), [ + [0, 1, 2], + [1, 2], + 'at SvelteSet.add', + [2], + 'at SvelteSet.add', + [], + 'at SvelteSet.add' + ]); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/inspect-derived-4/main.svelte b/packages/svelte/tests/runtime-runes/samples/inspect-derived-4/main.svelte new file mode 100644 index 0000000000..eb4ea891db --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/inspect-derived-4/main.svelte @@ -0,0 +1,14 @@ + + + diff --git a/packages/svelte/tests/runtime-runes/samples/inspect-new-property/_config.js b/packages/svelte/tests/runtime-runes/samples/inspect-new-property/_config.js index 8134044b16..43d217977e 100644 --- a/packages/svelte/tests/runtime-runes/samples/inspect-new-property/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/inspect-new-property/_config.js @@ -15,9 +15,9 @@ export default test({ {}, [], { x: 'hello' }, - 'at HTMLButtonElement.on_click', + 'at HTMLButtonElement.Main.button.__click', ['hello'], - 'at HTMLButtonElement.on_click' + 'at HTMLButtonElement.Main.button.__click' ]); } }); diff --git a/packages/svelte/tests/runtime-runes/samples/inspect-recursive/_config.js b/packages/svelte/tests/runtime-runes/samples/inspect-recursive/_config.js index 9d95956e7d..8bf67159f5 100644 --- a/packages/svelte/tests/runtime-runes/samples/inspect-recursive/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/inspect-recursive/_config.js @@ -15,9 +15,9 @@ export default test({ assert.deepEqual(normalise_inspect_logs(logs), [ [], [{}], - 'at HTMLButtonElement.on_click', + 'at HTMLButtonElement.Main.button.__click', [{}, {}], - 'at HTMLButtonElement.on_click' + 'at HTMLButtonElement.Main.button.__click' ]); } }); diff --git a/packages/svelte/tests/runtime-runes/samples/mount-props-updates/_config.js b/packages/svelte/tests/runtime-runes/samples/mount-props-updates/_config.js index ff7af2d524..57e4d276ff 100644 --- a/packages/svelte/tests/runtime-runes/samples/mount-props-updates/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/mount-props-updates/_config.js @@ -22,7 +22,7 @@ export default test({ target.innerHTML, // bar is not set in the parent because it's a readonly property // baz is not set in the parent because while it's a bindable property, - // it wasn't set initially so it's treated as a readonly proeprty + // it wasn't set initially so it's treated as a readonly property ` foo 3
    1 2 3 4
    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
    Link text Link text + + diff --git a/packages/svelte/tests/validator/samples/a11y-media-has-caption/input.svelte b/packages/svelte/tests/validator/samples/a11y-media-has-caption/input.svelte index 2e479d97da..9d83ca8771 100644 --- a/packages/svelte/tests/validator/samples/a11y-media-has-caption/input.svelte +++ b/packages/svelte/tests/validator/samples/a11y-media-has-caption/input.svelte @@ -1,6 +1,7 @@ - - - + + + - - + + + diff --git a/packages/svelte/tests/validator/samples/a11y-media-has-caption/warnings.json b/packages/svelte/tests/validator/samples/a11y-media-has-caption/warnings.json index 2a7de1f730..c596f23806 100644 --- a/packages/svelte/tests/validator/samples/a11y-media-has-caption/warnings.json +++ b/packages/svelte/tests/validator/samples/a11y-media-has-caption/warnings.json @@ -2,7 +2,7 @@ { "code": "a11y_media_has_caption", "end": { - "column": 15, + "column": 23, "line": 2 }, "message": "`