mirror of https://github.com/sveltejs/svelte
commit
07040d1005
@ -0,0 +1,5 @@
|
||||
---
|
||||
'svelte': patch
|
||||
---
|
||||
|
||||
fix: take into account static blocks when determining transition locality
|
||||
@ -0,0 +1,5 @@
|
||||
---
|
||||
'svelte': patch
|
||||
---
|
||||
|
||||
fix: flush pending changes after rendering `failed` snippet
|
||||
@ -1,5 +0,0 @@
|
||||
---
|
||||
'svelte': patch
|
||||
---
|
||||
|
||||
fix: improve `each_key_without_as` error
|
||||
@ -0,0 +1,185 @@
|
||||
/** @import { Effect, TemplateNode } from '#client' */
|
||||
import { is_runes } from '../../context.js';
|
||||
import { Batch, current_batch } from '../../reactivity/batch.js';
|
||||
import {
|
||||
branch,
|
||||
destroy_effect,
|
||||
move_effect,
|
||||
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';
|
||||
|
||||
/**
|
||||
* @typedef {{ effect: Effect, fragment: DocumentFragment }} Branch
|
||||
*/
|
||||
|
||||
/**
|
||||
* @template Key
|
||||
*/
|
||||
export class BranchManager {
|
||||
/** @type {TemplateNode} */
|
||||
anchor;
|
||||
|
||||
/** @type {Map<Batch, Key>} */
|
||||
#batches = new Map();
|
||||
|
||||
/** @type {Map<Key, Effect>} */
|
||||
#onscreen = new Map();
|
||||
|
||||
/** @type {Map<Key, Branch>} */
|
||||
#offscreen = new Map();
|
||||
|
||||
/**
|
||||
* Whether to pause (i.e. outro) on change, or destroy immediately.
|
||||
* This is necessary for `<svelte:element>`
|
||||
*/
|
||||
#transition = true;
|
||||
|
||||
/**
|
||||
* @param {TemplateNode} anchor
|
||||
* @param {boolean} transition
|
||||
*/
|
||||
constructor(anchor, transition = true) {
|
||||
this.anchor = anchor;
|
||||
this.#transition = transition;
|
||||
}
|
||||
|
||||
#commit = () => {
|
||||
var batch = /** @type {Batch} */ (current_batch);
|
||||
|
||||
// if this batch was made obsolete, bail
|
||||
if (!this.#batches.has(batch)) return;
|
||||
|
||||
var key = /** @type {Key} */ (this.#batches.get(batch));
|
||||
|
||||
var onscreen = this.#onscreen.get(key);
|
||||
|
||||
if (onscreen) {
|
||||
// effect is already in the DOM — abort any current outro
|
||||
resume_effect(onscreen);
|
||||
} else {
|
||||
// effect is currently offscreen. put it in the DOM
|
||||
var offscreen = this.#offscreen.get(key);
|
||||
|
||||
if (offscreen) {
|
||||
this.#onscreen.set(key, offscreen.effect);
|
||||
this.#offscreen.delete(key);
|
||||
|
||||
// remove the anchor...
|
||||
/** @type {TemplateNode} */ (offscreen.fragment.lastChild).remove();
|
||||
|
||||
// ...and append the fragment
|
||||
this.anchor.before(offscreen.fragment);
|
||||
onscreen = offscreen.effect;
|
||||
}
|
||||
}
|
||||
|
||||
for (const [b, k] of this.#batches) {
|
||||
this.#batches.delete(b);
|
||||
|
||||
if (b === batch) {
|
||||
// keep values for newer batches
|
||||
break;
|
||||
}
|
||||
|
||||
const offscreen = this.#offscreen.get(k);
|
||||
|
||||
if (offscreen) {
|
||||
// for older batches, destroy offscreen effects
|
||||
// as they will never be committed
|
||||
destroy_effect(offscreen.effect);
|
||||
this.#offscreen.delete(k);
|
||||
}
|
||||
}
|
||||
|
||||
// outro/destroy all onscreen effects...
|
||||
for (const [k, effect] of this.#onscreen) {
|
||||
// ...except the one that was just committed
|
||||
if (k === key) continue;
|
||||
|
||||
const on_destroy = () => {
|
||||
const keys = Array.from(this.#batches.values());
|
||||
|
||||
if (keys.includes(k)) {
|
||||
// keep the effect offscreen, as another batch will need it
|
||||
var fragment = document.createDocumentFragment();
|
||||
move_effect(effect, fragment);
|
||||
|
||||
fragment.append(create_text()); // TODO can we avoid this?
|
||||
|
||||
this.#offscreen.set(k, { effect, fragment });
|
||||
} else {
|
||||
destroy_effect(effect);
|
||||
}
|
||||
|
||||
this.#onscreen.delete(k);
|
||||
};
|
||||
|
||||
if (this.#transition || !onscreen) {
|
||||
pause_effect(effect, on_destroy, false);
|
||||
} else {
|
||||
on_destroy();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {any} key
|
||||
* @param {null | ((target: TemplateNode) => void)} fn
|
||||
*/
|
||||
ensure(key, fn) {
|
||||
var batch = /** @type {Batch} */ (current_batch);
|
||||
var defer = should_defer_append();
|
||||
|
||||
if (fn && !this.#onscreen.has(key) && !this.#offscreen.has(key)) {
|
||||
if (defer) {
|
||||
var fragment = document.createDocumentFragment();
|
||||
var target = create_text();
|
||||
|
||||
fragment.append(target);
|
||||
|
||||
this.#offscreen.set(key, {
|
||||
effect: branch(() => fn(target)),
|
||||
fragment
|
||||
});
|
||||
} else {
|
||||
this.#onscreen.set(
|
||||
key,
|
||||
branch(() => fn(this.anchor))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
this.#batches.set(batch, key);
|
||||
|
||||
if (defer) {
|
||||
for (const [k, effect] of this.#onscreen) {
|
||||
if (k === key) {
|
||||
batch.skipped_effects.delete(effect);
|
||||
} else {
|
||||
batch.skipped_effects.add(effect);
|
||||
}
|
||||
}
|
||||
|
||||
for (const [k, branch] of this.#offscreen) {
|
||||
if (k === key) {
|
||||
batch.skipped_effects.delete(branch.effect);
|
||||
} else {
|
||||
batch.skipped_effects.add(branch.effect);
|
||||
}
|
||||
}
|
||||
|
||||
batch.add_callback(this.#commit);
|
||||
} else {
|
||||
if (hydrating) {
|
||||
this.anchor = hydrate_node;
|
||||
}
|
||||
|
||||
this.#commit();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,5 @@
|
||||
import { test } from '../../test';
|
||||
|
||||
export default test({
|
||||
html: 'foo'
|
||||
});
|
||||
@ -0,0 +1 @@
|
||||
<slot data='foo'></slot>
|
||||
@ -0,0 +1,7 @@
|
||||
<script>
|
||||
import Component from './component.svelte';
|
||||
</script>
|
||||
<Component let:data>
|
||||
{@const thing = data}
|
||||
{thing}
|
||||
</Component>
|
||||
@ -0,0 +1,63 @@
|
||||
import { tick } from 'svelte';
|
||||
import { test } from '../../test';
|
||||
|
||||
export default test({
|
||||
async test({ assert, target }) {
|
||||
const [increment, shift] = target.querySelectorAll('button');
|
||||
|
||||
shift.click();
|
||||
await tick();
|
||||
|
||||
shift.click();
|
||||
await tick();
|
||||
|
||||
assert.htmlEqual(
|
||||
target.innerHTML,
|
||||
`
|
||||
<button>0</button>
|
||||
<button>shift</button>
|
||||
<p>even</p>
|
||||
<p>0</p>
|
||||
`
|
||||
);
|
||||
|
||||
increment.click();
|
||||
await tick();
|
||||
|
||||
assert.htmlEqual(
|
||||
target.innerHTML,
|
||||
`
|
||||
<button>1</button>
|
||||
<button>shift</button>
|
||||
<p>even</p>
|
||||
<p>0</p>
|
||||
`
|
||||
);
|
||||
|
||||
shift.click();
|
||||
await tick();
|
||||
|
||||
assert.htmlEqual(
|
||||
target.innerHTML,
|
||||
`
|
||||
<button>1</button>
|
||||
<button>shift</button>
|
||||
<p>odd</p>
|
||||
<p>loading...</p>
|
||||
`
|
||||
);
|
||||
|
||||
shift.click();
|
||||
await tick();
|
||||
|
||||
assert.htmlEqual(
|
||||
target.innerHTML,
|
||||
`
|
||||
<button>1</button>
|
||||
<button>shift</button>
|
||||
<p>odd</p>
|
||||
<p>1</p>
|
||||
`
|
||||
);
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,36 @@
|
||||
<script>
|
||||
let resolvers = [];
|
||||
|
||||
function push(value) {
|
||||
const { promise, resolve } = Promise.withResolvers();
|
||||
resolvers.push(() => resolve(value));
|
||||
return promise;
|
||||
}
|
||||
|
||||
let count = $state(0);
|
||||
</script>
|
||||
|
||||
<button onclick={() => count += 1}>{$state.eager(count)}</button>
|
||||
<button onclick={() => resolvers.shift()?.()}>shift</button>
|
||||
|
||||
<svelte:boundary>
|
||||
{#if await push(count) % 2 === 0}
|
||||
<p>even</p>
|
||||
{:else}
|
||||
<p>odd</p>
|
||||
{/if}
|
||||
|
||||
{#key count}
|
||||
<svelte:boundary>
|
||||
<p>{await push(count)}</p>
|
||||
|
||||
{#snippet pending()}
|
||||
<p>loading...</p>
|
||||
{/snippet}
|
||||
</svelte:boundary>
|
||||
{/key}
|
||||
|
||||
{#snippet pending()}
|
||||
<p>loading...</p>
|
||||
{/snippet}
|
||||
</svelte:boundary>
|
||||
@ -1,7 +1,11 @@
|
||||
<script>
|
||||
$effect(() => {
|
||||
console.log('before');
|
||||
});
|
||||
|
||||
await 1;
|
||||
|
||||
$effect(() => {
|
||||
console.log('hello');
|
||||
console.log('after');
|
||||
});
|
||||
</script>
|
||||
|
||||
@ -0,0 +1,5 @@
|
||||
<script>
|
||||
$effect(() => {
|
||||
console.log('in effect')
|
||||
});
|
||||
</script>
|
||||
@ -0,0 +1,16 @@
|
||||
import { tick } from 'svelte';
|
||||
import { test } from '../../test';
|
||||
|
||||
export default test({
|
||||
async test({ assert, target, logs }) {
|
||||
const [shift] = target.querySelectorAll('button');
|
||||
|
||||
await tick();
|
||||
assert.deepEqual(logs, []);
|
||||
|
||||
shift.click();
|
||||
await tick();
|
||||
|
||||
assert.deepEqual(logs, ['in effect']);
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,22 @@
|
||||
<script>
|
||||
import Child from './Child.svelte';
|
||||
|
||||
let resolvers = [];
|
||||
|
||||
function push(value) {
|
||||
const { promise, resolve } = Promise.withResolvers();
|
||||
resolvers.push(() => resolve(value));
|
||||
return promise;
|
||||
}
|
||||
</script>
|
||||
|
||||
<button onclick={() => resolvers.shift()?.()}>shift</button>
|
||||
|
||||
<svelte:boundary>
|
||||
<p>{await push('hello')}</p>
|
||||
<Child />
|
||||
|
||||
{#snippet pending()}
|
||||
<p>loading...</p>
|
||||
{/snippet}
|
||||
</svelte:boundary>
|
||||
@ -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']);
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,20 @@
|
||||
<script>
|
||||
let fail = $state(false);
|
||||
|
||||
function error() {
|
||||
throw new Error('oops');
|
||||
}
|
||||
|
||||
function attachment() {
|
||||
console.log('attachment');
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:boundary>
|
||||
{fail ? error() : 'all good'}
|
||||
<button onclick={() => fail = true}>fail</button>
|
||||
|
||||
{#snippet failed()}
|
||||
<div {@attach attachment}>oops!</div>
|
||||
{/snippet}
|
||||
</svelte:boundary>
|
||||
@ -0,0 +1,20 @@
|
||||
import { test } from '../../test';
|
||||
import { flushSync } from 'svelte';
|
||||
|
||||
export default test({
|
||||
mode: ['client'],
|
||||
async test({ target, assert, logs }) {
|
||||
const button = target.querySelector('button');
|
||||
|
||||
button?.click();
|
||||
flushSync();
|
||||
button?.click();
|
||||
flushSync();
|
||||
button?.click();
|
||||
flushSync();
|
||||
button?.click();
|
||||
flushSync();
|
||||
|
||||
assert.deepEqual(logs, ['two', 'one', 'two', 'one', 'two']);
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,18 @@
|
||||
<script lang="ts">
|
||||
let b = $state(false);
|
||||
let v = $state("two");
|
||||
|
||||
$effect(() => {
|
||||
v = b ? "one" : "two";
|
||||
})
|
||||
</script>
|
||||
|
||||
<button onclick={() => b = !b}>Trigger</button>
|
||||
|
||||
{#if v === "one"}
|
||||
<div>if1 matched! {console.log('one')}</div>
|
||||
{:else if v === "two"}
|
||||
<div>if2 matched! {console.log('two')}</div>
|
||||
{:else}
|
||||
<div>nothing matched {console.log('else')}</div>
|
||||
{/if}
|
||||
@ -0,0 +1,13 @@
|
||||
import { test } from '../../test';
|
||||
import { flushSync } from 'svelte';
|
||||
|
||||
export default test({
|
||||
mode: ['client'],
|
||||
async test({ target, assert }) {
|
||||
const button = target.querySelector('button');
|
||||
|
||||
flushSync(() => button?.click());
|
||||
|
||||
assert.equal(target.textContent?.trim(), 'Trigger');
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,18 @@
|
||||
<script>
|
||||
let centerRow = $state({ nested: { optional: 2, required: 3 } });
|
||||
|
||||
let someChange = $state(false);
|
||||
$effect(() => {
|
||||
if (someChange) centerRow = undefined;
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if centerRow?.nested}
|
||||
{#if centerRow?.nested?.optional != undefined && centerRow.nested.optional > 0}
|
||||
op: {centerRow.nested.optional}<br />
|
||||
{:else}
|
||||
req: {centerRow.nested.required}<br />
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<button onclick={() => (someChange = true)}>Trigger</button>
|
||||
@ -0,0 +1,6 @@
|
||||
<script>
|
||||
let { p } = $props();
|
||||
$effect.pre(() => {
|
||||
console.log('running ' + p)
|
||||
})
|
||||
</script>
|
||||
@ -0,0 +1,13 @@
|
||||
import { flushSync } from 'svelte';
|
||||
import { test } from '../../test';
|
||||
|
||||
export default test({
|
||||
mode: ['client'],
|
||||
async test({ assert, target, logs }) {
|
||||
const button = target.querySelector('button');
|
||||
|
||||
button?.click();
|
||||
flushSync();
|
||||
assert.deepEqual(logs, ['pre', 'running b', 'pre', 'pre']);
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,18 @@
|
||||
<script>
|
||||
import Component from './Component.svelte';
|
||||
|
||||
let p = $state('b');
|
||||
|
||||
$effect.pre(() => {
|
||||
console.log('pre')
|
||||
if (p === 'a') p = null;
|
||||
})
|
||||
</script>
|
||||
|
||||
{#if p || !p}
|
||||
{#if p}
|
||||
<Component {p} />
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<button onclick={() => p = 'a'}>a</button>
|
||||
@ -0,0 +1,22 @@
|
||||
import { flushSync } from 'svelte';
|
||||
import { test } from '../../test';
|
||||
|
||||
export default test({
|
||||
async test({ assert, target }) {
|
||||
const btn = target.querySelector('button');
|
||||
|
||||
btn?.click();
|
||||
flushSync();
|
||||
assert.htmlEqual(
|
||||
target.innerHTML,
|
||||
`
|
||||
<button>Toggle</button>
|
||||
<div>Should not transition out</div>
|
||||
`
|
||||
);
|
||||
|
||||
btn?.click();
|
||||
flushSync();
|
||||
assert.htmlEqual(target.innerHTML, '<button>Toggle</button>');
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,18 @@
|
||||
|
||||
<script>
|
||||
import { slide } from 'svelte/transition';
|
||||
let showText = $state(false);
|
||||
let show = $state(true);
|
||||
</script>
|
||||
|
||||
<button onclick={() => showText = !showText}>
|
||||
Toggle
|
||||
</button>
|
||||
|
||||
{#if showText}
|
||||
{#if show}
|
||||
<div transition:slide>
|
||||
Should not transition out
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
@ -0,0 +1,3 @@
|
||||
import { test } from '../../test';
|
||||
|
||||
export default test({ compileOptions: { experimental: { async: true } } });
|
||||
@ -0,0 +1,52 @@
|
||||
import 'svelte/internal/disclose-version';
|
||||
import 'svelte/internal/flags/async';
|
||||
import * as $ from 'svelte/internal/client';
|
||||
|
||||
export default function Async_in_derived($$anchor, $$props) {
|
||||
$.push($$props, true);
|
||||
|
||||
$.async_body($$anchor, async ($$anchor) => {
|
||||
let yes1 = (await $.save($.async_derived(async () => (await $.save(1))())))();
|
||||
let yes2 = (await $.save($.async_derived(async () => foo((await $.save(1))()))))();
|
||||
|
||||
let no1 = $.derived(async () => {
|
||||
return await 1;
|
||||
});
|
||||
|
||||
let no2 = $.derived(() => async () => {
|
||||
return await 1;
|
||||
});
|
||||
|
||||
if ($.aborted()) return;
|
||||
|
||||
var fragment = $.comment();
|
||||
var node = $.first_child(fragment);
|
||||
|
||||
{
|
||||
var consequent = ($$anchor) => {
|
||||
$.async_body($$anchor, async ($$anchor) => {
|
||||
const yes1 = (await $.save($.async_derived(async () => (await $.save(1))())))();
|
||||
const yes2 = (await $.save($.async_derived(async () => foo((await $.save(1))()))))();
|
||||
|
||||
const no1 = $.derived(() => (async () => {
|
||||
return await 1;
|
||||
})());
|
||||
|
||||
const no2 = $.derived(() => (async () => {
|
||||
return await 1;
|
||||
})());
|
||||
|
||||
if ($.aborted()) return;
|
||||
});
|
||||
};
|
||||
|
||||
$.if(node, ($$render) => {
|
||||
if (true) $$render(consequent);
|
||||
});
|
||||
}
|
||||
|
||||
$.append($$anchor, fragment);
|
||||
});
|
||||
|
||||
$.pop();
|
||||
}
|
||||
@ -0,0 +1,40 @@
|
||||
import 'svelte/internal/flags/async';
|
||||
import * as $ from 'svelte/internal/server';
|
||||
|
||||
export default function Async_in_derived($$renderer, $$props) {
|
||||
$$renderer.component(($$renderer) => {
|
||||
$$renderer.async(async ($$renderer) => {
|
||||
let yes1 = (await $.save(1))();
|
||||
let yes2 = foo((await $.save(1))());
|
||||
|
||||
let no1 = (async () => {
|
||||
return await 1;
|
||||
})();
|
||||
|
||||
let no2 = async () => {
|
||||
return await 1;
|
||||
};
|
||||
|
||||
$$renderer.async(async ($$renderer) => {
|
||||
if (true) {
|
||||
$$renderer.push('<!--[-->');
|
||||
|
||||
const yes1 = (await $.save(1))();
|
||||
const yes2 = foo((await $.save(1))());
|
||||
|
||||
const no1 = (async () => {
|
||||
return await 1;
|
||||
})();
|
||||
|
||||
const no2 = (async () => {
|
||||
return await 1;
|
||||
})();
|
||||
} else {
|
||||
$$renderer.push('<!--[!-->');
|
||||
}
|
||||
});
|
||||
|
||||
$$renderer.push(`<!--]-->`);
|
||||
});
|
||||
});
|
||||
}
|
||||
@ -0,0 +1,21 @@
|
||||
<script>
|
||||
let yes1 = $derived(await 1);
|
||||
let yes2 = $derived(foo(await 1));
|
||||
let no1 = $derived.by(async () => {
|
||||
return await 1;
|
||||
});
|
||||
let no2 = $derived(async () => {
|
||||
return await 1;
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if true}
|
||||
{@const yes1 = await 1}
|
||||
{@const yes2 = foo(await 1)}
|
||||
{@const no1 = (async () => {
|
||||
return await 1;
|
||||
})()}
|
||||
{@const no2 = (async () => {
|
||||
return await 1;
|
||||
})()}
|
||||
{/if}
|
||||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in new issue