mirror of https://github.com/sveltejs/svelte
commit
42b3c68eac
@ -0,0 +1,5 @@
|
||||
---
|
||||
'svelte': patch
|
||||
---
|
||||
|
||||
fix: discard batches made obsolete by commit
|
||||
@ -1,5 +0,0 @@
|
||||
---
|
||||
'svelte': patch
|
||||
---
|
||||
|
||||
fix: properly lazily evaluate RHS when checking for `assignment_value_stale`
|
||||
@ -1,5 +0,0 @@
|
||||
---
|
||||
'svelte': patch
|
||||
---
|
||||
|
||||
chore: rebase batches after process, not during
|
||||
@ -0,0 +1,69 @@
|
||||
name: Autofix Lint
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
autofix-lint:
|
||||
permissions:
|
||||
contents: write # to push the generated types commit
|
||||
pull-requests: read # to resolve the PR head ref
|
||||
# prevents this action from running on forks
|
||||
if: |
|
||||
github.repository == 'sveltejs/svelte' &&
|
||||
(
|
||||
github.event_name == 'workflow_dispatch' ||
|
||||
(
|
||||
github.event.issue.pull_request != null &&
|
||||
github.event.comment.body == '/autofix' &&
|
||||
contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.comment.author_association)
|
||||
)
|
||||
)
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Get PR ref
|
||||
if: github.event_name != 'workflow_dispatch'
|
||||
id: pr
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
script: |
|
||||
const { data: pull } = await github.rest.pulls.get({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
pull_number: context.issue.number
|
||||
});
|
||||
if (pull.head.repo.full_name !== `${context.repo.owner}/${context.repo.repo}`) {
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
body: 'Cannot autofix: this PR is from a forked repository. The autofix workflow can only push to branches within this repository.'
|
||||
});
|
||||
core.setFailed('PR is from a fork');
|
||||
}
|
||||
core.setOutput('ref', pull.head.ref);
|
||||
- uses: actions/checkout@v6
|
||||
if: github.event_name == 'workflow_dispatch' || steps.pr.outcome == 'success'
|
||||
with:
|
||||
ref: ${{ github.event_name == 'workflow_dispatch' && github.ref || steps.pr.outputs.ref }}
|
||||
- uses: pnpm/action-setup@v4.3.0
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 24
|
||||
cache: pnpm
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- name: Build
|
||||
run: pnpm -F svelte build
|
||||
- name: Run prettier
|
||||
run: pnpm format
|
||||
- name: Commit changes
|
||||
run: |
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git add -A
|
||||
git diff --staged --quiet || git commit -m "chore: autofix"
|
||||
git push origin HEAD
|
||||
@ -0,0 +1,25 @@
|
||||
import { tick } from 'svelte';
|
||||
import { test } from '../../test';
|
||||
|
||||
export default test({
|
||||
compileOptions: {
|
||||
dev: true
|
||||
},
|
||||
async test({ assert, target }) {
|
||||
const button = /** @type {HTMLElement} */ (target.querySelector('button'));
|
||||
await tick();
|
||||
assert.htmlEqual(target.innerHTML, `<button>go</button><p>count1: 0, count2: 0</p>`);
|
||||
|
||||
button.click();
|
||||
await tick();
|
||||
assert.htmlEqual(target.innerHTML, `<button>go</button><p>count1: 1, count2: 1</p>`);
|
||||
|
||||
// additional tick necessary in legacy mode because it's using Promise.resolve() which finishes before the await in the component,
|
||||
// causing the cache to not be set yet, which would result in count2 becoming 2
|
||||
await tick();
|
||||
|
||||
button.click();
|
||||
await tick();
|
||||
assert.htmlEqual(target.innerHTML, `<button>go</button><p>count1: 2, count2: 1</p>`);
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,18 @@
|
||||
<script>
|
||||
let count1 = $state(0);
|
||||
let count2 = $state(0);
|
||||
let cache = $state({});
|
||||
|
||||
async function go() {
|
||||
count1++;
|
||||
const value = cache.value ??= await get_value();
|
||||
}
|
||||
|
||||
function get_value() {
|
||||
count2++;
|
||||
return 42;
|
||||
}
|
||||
</script>
|
||||
|
||||
<button onclick={go}>go</button>
|
||||
<p>count1: {count1}, count2: {count2}</p>
|
||||
@ -0,0 +1,89 @@
|
||||
import { tick } from 'svelte';
|
||||
import { test } from '../../test';
|
||||
|
||||
export default test({
|
||||
async test({ assert, target }) {
|
||||
await tick();
|
||||
|
||||
const [increment, shift, pop] = target.querySelectorAll('button');
|
||||
|
||||
assert.htmlEqual(
|
||||
target.innerHTML,
|
||||
`
|
||||
<button>1</button>
|
||||
<button>shift</button>
|
||||
<button>pop</button>
|
||||
<p>1 = 1</p>
|
||||
`
|
||||
);
|
||||
|
||||
increment.click();
|
||||
await tick();
|
||||
increment.click();
|
||||
await tick();
|
||||
|
||||
assert.htmlEqual(
|
||||
target.innerHTML,
|
||||
`
|
||||
<button>3</button>
|
||||
<button>shift</button>
|
||||
<button>pop</button>
|
||||
<p>1 = 1</p>
|
||||
`
|
||||
);
|
||||
|
||||
shift.click();
|
||||
await tick();
|
||||
|
||||
assert.htmlEqual(
|
||||
target.innerHTML,
|
||||
`
|
||||
<button>3</button>
|
||||
<button>shift</button>
|
||||
<button>pop</button>
|
||||
<p>1 = 1</p>
|
||||
`
|
||||
);
|
||||
|
||||
shift.click();
|
||||
await tick();
|
||||
|
||||
assert.htmlEqual(
|
||||
target.innerHTML,
|
||||
`
|
||||
<button>3</button>
|
||||
<button>shift</button>
|
||||
<button>pop</button>
|
||||
<p>3 = 3</p>
|
||||
`
|
||||
);
|
||||
|
||||
increment.click();
|
||||
await tick();
|
||||
increment.click();
|
||||
await tick();
|
||||
|
||||
assert.htmlEqual(
|
||||
target.innerHTML,
|
||||
`
|
||||
<button>5</button>
|
||||
<button>shift</button>
|
||||
<button>pop</button>
|
||||
<p>3 = 3</p>
|
||||
`
|
||||
);
|
||||
|
||||
pop.click();
|
||||
await tick();
|
||||
|
||||
assert.htmlEqual(
|
||||
target.innerHTML,
|
||||
`
|
||||
<button>5</button>
|
||||
<button>shift</button>
|
||||
<button>pop</button>
|
||||
<p>5 = 5</p>
|
||||
`
|
||||
);
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,36 @@
|
||||
<script>
|
||||
import { getAbortSignal } from 'svelte';
|
||||
|
||||
const queue = [];
|
||||
|
||||
function push(value) {
|
||||
if (value === 1) return 1;
|
||||
const d = Promise.withResolvers();
|
||||
|
||||
queue.push(() => d.resolve(value));
|
||||
|
||||
const signal = getAbortSignal();
|
||||
signal.onabort = () => d.reject(signal.reason);
|
||||
|
||||
return d.promise;
|
||||
}
|
||||
|
||||
function shift() {
|
||||
queue.shift()?.();
|
||||
}
|
||||
|
||||
function pop() {
|
||||
queue.pop()?.();
|
||||
}
|
||||
|
||||
let n = $state(1);
|
||||
</script>
|
||||
|
||||
<button onclick={() => n++}>
|
||||
{$state.eager(n)}
|
||||
</button>
|
||||
|
||||
<button onclick={shift}>shift</button>
|
||||
<button onclick={pop}>pop</button>
|
||||
|
||||
<p>{n} = {await push(n)}</p>
|
||||
@ -0,0 +1,23 @@
|
||||
import { tick } from 'svelte';
|
||||
import { test } from '../../test';
|
||||
|
||||
export default test({
|
||||
async test({ assert, target }) {
|
||||
await tick();
|
||||
const [increment, shift] = target.querySelectorAll('button');
|
||||
|
||||
increment.click();
|
||||
await tick();
|
||||
assert.htmlEqual(
|
||||
target.innerHTML,
|
||||
`<button>clicks: 0 - 0 - 0</button> <button>shift</button> <p>true - true</p>`
|
||||
);
|
||||
|
||||
shift.click();
|
||||
await tick();
|
||||
assert.htmlEqual(
|
||||
target.innerHTML,
|
||||
`<button>clicks: 1 - 1 - 1</button> <button>shift</button> <p>false - false</p>`
|
||||
);
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,22 @@
|
||||
<script>
|
||||
|
||||
let count = $state(0);
|
||||
const delayedCount = $derived(await push(count));
|
||||
const derivedCount = $derived(count);
|
||||
|
||||
let resolvers = [];
|
||||
|
||||
function push(value) {
|
||||
if (!value) return value;
|
||||
const { promise, resolve } = Promise.withResolvers();
|
||||
resolvers.push(() => resolve(value));
|
||||
return promise;
|
||||
}
|
||||
</script>
|
||||
|
||||
<button onclick={() => count += 1}>
|
||||
clicks: {count} - {delayedCount} - {derivedCount}
|
||||
</button>
|
||||
<button onclick={() => resolvers.shift()?.()}>shift</button>
|
||||
|
||||
<p>{$state.eager(count) !== count} - {$state.eager(derivedCount) !== derivedCount}</p>
|
||||
@ -0,0 +1,16 @@
|
||||
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();
|
||||
assert.deepEqual(logs, [10]);
|
||||
|
||||
btn.click();
|
||||
await tick();
|
||||
assert.deepEqual(logs, [10, 10]);
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,21 @@
|
||||
<script>
|
||||
import { fork } from 'svelte';
|
||||
|
||||
let s = $state(1);
|
||||
let d = $derived(s * 10);
|
||||
</script>
|
||||
|
||||
<button
|
||||
onclick={() => {
|
||||
const f = fork(() => {
|
||||
// d has not been read yet, so this write happens with an uninitialized old value
|
||||
s = 2;
|
||||
d = 99;
|
||||
});
|
||||
|
||||
f.discard();
|
||||
console.log(d);
|
||||
}}
|
||||
>
|
||||
test
|
||||
</button>
|
||||
@ -0,0 +1,11 @@
|
||||
<script lang="ts">
|
||||
let x = $state(false);
|
||||
|
||||
$effect(() => {
|
||||
x = true;
|
||||
|
||||
return () => {
|
||||
x = false;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@ -0,0 +1,16 @@
|
||||
import { tick } from 'svelte';
|
||||
import { test } from '../../test';
|
||||
|
||||
export default test({
|
||||
async test({ assert, target }) {
|
||||
// This test causes two batches to be scheduled such that the same root is traversed multiple times,
|
||||
// some of the time while it was already marked clean by a previous batch processing. It tests
|
||||
// that the app stays reactive after, i.e. that the root is not improperly marked as unclean.
|
||||
await tick();
|
||||
const [button] = target.querySelectorAll('button');
|
||||
|
||||
button.click();
|
||||
await tick();
|
||||
assert.htmlEqual(target.innerHTML, `<button>toggle</button><p>hello</p>`);
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,19 @@
|
||||
<script lang="ts">
|
||||
import Component from './Component.svelte';
|
||||
|
||||
let condition = $state(false);
|
||||
</script>
|
||||
|
||||
<button onclick={() => (condition = !condition)}>toggle</button>
|
||||
|
||||
<svelte:boundary>
|
||||
<Component whatever={await 1} />
|
||||
|
||||
{#snippet pending()}
|
||||
<Component />
|
||||
{/snippet}
|
||||
</svelte:boundary>
|
||||
|
||||
{#if condition}
|
||||
<p>hello</p>
|
||||
{/if}
|
||||
@ -0,0 +1,54 @@
|
||||
import { tick } from 'svelte';
|
||||
import { test } from '../../test';
|
||||
|
||||
export default test({
|
||||
async test({ assert, target }) {
|
||||
const [add, shift, reset] = target.querySelectorAll('button');
|
||||
|
||||
// resolve initial pending state
|
||||
shift.click();
|
||||
await tick();
|
||||
|
||||
const [p] = target.querySelectorAll('p');
|
||||
|
||||
const select = /** @type {HTMLSelectElement} */ (target.querySelector('select'));
|
||||
assert.equal(select.value, 'a');
|
||||
|
||||
// add option 'c', making items ['a', 'b', 'c']
|
||||
add.click();
|
||||
await tick();
|
||||
|
||||
// select 'b' while focused
|
||||
select.focus();
|
||||
select.value = 'b';
|
||||
select.dispatchEvent(new InputEvent('change', { bubbles: true }));
|
||||
await tick();
|
||||
|
||||
assert.equal(select.value, 'b');
|
||||
assert.equal(p.textContent, 'a');
|
||||
|
||||
// add option 'd', making items ['a', 'b', 'c', 'd']
|
||||
// this triggers MutationObserver which uses select.__value
|
||||
add.click();
|
||||
await tick();
|
||||
|
||||
// select should still show 'b', not snap to a stale value
|
||||
assert.equal(select.value, 'b');
|
||||
assert.equal(p.textContent, 'a');
|
||||
|
||||
shift.click();
|
||||
await tick();
|
||||
assert.equal(select.value, 'b');
|
||||
assert.equal(p.textContent, 'b');
|
||||
|
||||
reset.click();
|
||||
await tick();
|
||||
assert.equal(select.value, 'b');
|
||||
assert.equal(p.textContent, 'b');
|
||||
|
||||
shift.click();
|
||||
await tick();
|
||||
assert.equal(select.value, 'a');
|
||||
assert.equal(p.textContent, 'a');
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,31 @@
|
||||
<script lang="ts">
|
||||
let selected = $state('a');
|
||||
let items = $state(['a', 'b']);
|
||||
|
||||
let resolvers = [];
|
||||
let select;
|
||||
|
||||
function push(value) {
|
||||
const { promise, resolve } = Promise.withResolvers();
|
||||
resolvers.push(() => resolve(value));
|
||||
return promise;
|
||||
}
|
||||
</script>
|
||||
|
||||
<button onclick={() => items.push(String.fromCharCode(97 + items.length))}>add</button>
|
||||
<button onclick={() => resolvers.shift()?.()}>shift</button>
|
||||
<button onclick={() => selected = 'a'}>reset</button>
|
||||
|
||||
<svelte:boundary>
|
||||
<select bind:this={select} bind:value={selected}>
|
||||
{#each items as item}
|
||||
<option value={item}>{item}</option>
|
||||
{/each}
|
||||
</select>
|
||||
|
||||
<p>{await push(selected)}</p>
|
||||
|
||||
{#snippet pending()}
|
||||
<p>loading...</p>
|
||||
{/snippet}
|
||||
</svelte:boundary>
|
||||
@ -0,0 +1,5 @@
|
||||
<script lang="ts">
|
||||
$effect(() => {
|
||||
console.log('hello from child');
|
||||
});
|
||||
</script>
|
||||
@ -0,0 +1,7 @@
|
||||
import { test } from '../../test';
|
||||
|
||||
export default test({
|
||||
async test({ assert, logs }) {
|
||||
assert.deepEqual(logs, ['hello from child']);
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,11 @@
|
||||
<script>
|
||||
import Child from './Child.svelte';
|
||||
</script>
|
||||
|
||||
<svelte:boundary>
|
||||
<Child />
|
||||
|
||||
{#snippet pending()}
|
||||
<p>Loading...</p>
|
||||
{/snippet}
|
||||
</svelte:boundary>
|
||||
@ -0,0 +1,32 @@
|
||||
import { flushSync } from 'svelte';
|
||||
import { test } from '../../test';
|
||||
|
||||
export default test({
|
||||
async test({ assert, target, compileOptions }) {
|
||||
const [toggle, increment] = target.querySelectorAll('button');
|
||||
|
||||
flushSync(() => increment.click());
|
||||
assert.htmlEqual(
|
||||
target.innerHTML,
|
||||
`
|
||||
<button>toggle</button>
|
||||
<button>count: 1</button>
|
||||
<p>show: false</p>
|
||||
`
|
||||
);
|
||||
|
||||
assert.throws(() => {
|
||||
flushSync(() => toggle.click());
|
||||
}, /NonExistent is not defined/);
|
||||
|
||||
flushSync(() => increment.click());
|
||||
assert.htmlEqual(
|
||||
target.innerHTML,
|
||||
`
|
||||
<button>toggle</button>
|
||||
<button>count: 2</button>
|
||||
<p>show: ${compileOptions.experimental?.async ? 'false' : 'true'}</p>
|
||||
`
|
||||
);
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,13 @@
|
||||
<script>
|
||||
let show = $state(false);
|
||||
let count = $state(0);
|
||||
</script>
|
||||
|
||||
<button onclick={() => show = !show}>toggle</button>
|
||||
<button onclick={() => count += 1}>count: {count}</button>
|
||||
|
||||
<p>show: {show}</p>
|
||||
|
||||
{#if show}
|
||||
<NonExistent />
|
||||
{/if}
|
||||
@ -0,0 +1,49 @@
|
||||
import { flushSync, tick } from 'svelte';
|
||||
import { test } from '../../test';
|
||||
|
||||
export default test({
|
||||
async test({ assert, target, variant }) {
|
||||
const [button] = target.querySelectorAll('button');
|
||||
const [select] = target.querySelectorAll('select');
|
||||
|
||||
flushSync(() => {
|
||||
select.focus();
|
||||
select.value = '2';
|
||||
select.dispatchEvent(new InputEvent('change', { bubbles: true }));
|
||||
});
|
||||
|
||||
assert.equal(select.selectedOptions[0].textContent, '2');
|
||||
|
||||
assert.htmlEqual(
|
||||
target.innerHTML,
|
||||
`
|
||||
<button>add option</button>
|
||||
<p>selected: 2</p>
|
||||
<select>
|
||||
<option${variant === 'hydrate' ? ' selected=""' : ''}>1</option>
|
||||
<option>2</option>
|
||||
<option>3</option>
|
||||
</select>
|
||||
`
|
||||
);
|
||||
|
||||
flushSync(() => button.click());
|
||||
await tick();
|
||||
|
||||
assert.equal(select.selectedOptions[0].textContent, '2');
|
||||
|
||||
assert.htmlEqual(
|
||||
target.innerHTML,
|
||||
`
|
||||
<button>add option</button>
|
||||
<p>selected: 2</p>
|
||||
<select>
|
||||
<option${variant === 'hydrate' ? ' selected=""' : ''}>1</option>
|
||||
<option>2</option>
|
||||
<option>3</option>
|
||||
<option>4</option>
|
||||
</select>
|
||||
`
|
||||
);
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,16 @@
|
||||
<script>
|
||||
let options = $state([1, 2, 3]);
|
||||
let selected = $state(1);
|
||||
</script>
|
||||
|
||||
<button onclick={() => options.push(options.length + 1)}>
|
||||
add option
|
||||
</button>
|
||||
|
||||
<p>selected: {selected}</p>
|
||||
|
||||
<select bind:value={selected}>
|
||||
{#each options as o}
|
||||
<option>{o}</option>
|
||||
{/each}
|
||||
</select>
|
||||
@ -0,0 +1,19 @@
|
||||
import { flushSync } from 'svelte';
|
||||
import { test } from '../../test';
|
||||
|
||||
// While we don't officially document it, `untrack` also allows to opt out of the "unsafe mutation" validation, which is what we test here
|
||||
export default test({
|
||||
html: '<button>0 0 0</button>',
|
||||
test({ assert, target }) {
|
||||
const button = target.querySelector('button');
|
||||
|
||||
flushSync(() => button?.click());
|
||||
|
||||
assert.htmlEqual(
|
||||
target.innerHTML,
|
||||
`
|
||||
<button>1 1 2</button>
|
||||
`
|
||||
);
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,14 @@
|
||||
<script>
|
||||
import { untrack } from "svelte";
|
||||
|
||||
let count = $state(0);
|
||||
let mirrored = $state(0);
|
||||
let double = $derived.by(() => {
|
||||
untrack(() => {
|
||||
mirrored = count;
|
||||
});
|
||||
return count * 2;
|
||||
})
|
||||
</script>
|
||||
|
||||
<button onclick={() => count++}>{count} {mirrored} {double}</button>
|
||||
Loading…
Reference in new issue