Merge branch 'main' into parallelize-async-work

parallelize-async-work
ComputerGuy 4 days ago
commit d4d82532ce

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: replace `undefined` with `void(0)` in CallExpressions

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: place store setup inside async body

@ -4,7 +4,7 @@ title: Custom elements
<!-- - [basically what we have today](https://svelte.dev/docs/custom-elements-api) -->
Svelte components can also be compiled to custom elements (aka web components) using the `customElement: true` compiler option. You should specify a tag name for the component using the `<svelte:options>` [element](svelte-options).
Svelte components can also be compiled to custom elements (aka web components) using the `customElement: true` compiler option. You should specify a tag name for the component using the `<svelte:options>` [element](svelte-options). Within the custom element you can access the host element via the [`$host`](https://svelte.dev/docs/svelte/$host) rune.
```svelte
<svelte:options customElement="my-element" />

@ -1,5 +1,31 @@
# svelte
## 5.38.6
### Patch Changes
- fix: don't fail on `flushSync` while flushing effects ([#16674](https://github.com/sveltejs/svelte/pull/16674))
## 5.38.5
### Patch Changes
- fix: ensure async deriveds always get dependencies from thennable ([#16672](https://github.com/sveltejs/svelte/pull/16672))
## 5.38.4
### Patch Changes
- fix: place instance-level snippets inside async body ([#16666](https://github.com/sveltejs/svelte/pull/16666))
- fix: Add check for builtin custom elements in `set_custom_element_data` ([#16592](https://github.com/sveltejs/svelte/pull/16592))
- fix: restore batch along with effect context ([#16668](https://github.com/sveltejs/svelte/pull/16668))
- fix: wait until changes propagate before updating input selection state ([#16649](https://github.com/sveltejs/svelte/pull/16649))
- fix: add "Accept-CH" as valid value for `http-equiv` ([#16671](https://github.com/sveltejs/svelte/pull/16671))
## 5.38.3
### Patch Changes

@ -1268,6 +1268,7 @@ export interface HTMLMetaAttributes extends HTMLAttributes<HTMLMetaElement> {
charset?: string | undefined | null;
content?: string | undefined | null;
'http-equiv'?:
| 'accept-ch'
| 'content-security-policy'
| 'content-type'
| 'default-style'

@ -2,7 +2,7 @@
"name": "svelte",
"description": "Cybernetically enhanced web apps",
"license": "MIT",
"version": "5.38.3",
"version": "5.38.6",
"type": "module",
"types": "./types/index.d.ts",
"engines": {

@ -362,22 +362,41 @@ export function client_component(analysis, options) {
let component_block = b.block([
store_init,
...store_setup,
...legacy_reactive_declarations,
...group_binding_declarations,
...state.instance_level_snippets
...group_binding_declarations
]);
const should_inject_context =
dev ||
analysis.needs_context ||
analysis.reactive_statements.size > 0 ||
component_returned_object.length > 0;
if (analysis.instance.has_await) {
if (should_inject_context && component_returned_object.length > 0) {
component_block.body.push(b.var('$$exports'));
}
const body = b.block([
...store_setup,
...state.instance_level_snippets,
.../** @type {ESTree.Statement[]} */ (instance.body),
...(should_inject_context && component_returned_object.length > 0
? [b.stmt(b.assignment('=', b.id('$$exports'), b.object(component_returned_object)))]
: []),
b.if(b.call('$.aborted'), b.return()),
.../** @type {ESTree.Statement[]} */ (template.body)
]);
component_block.body.push(b.stmt(b.call(`$.async_body`, b.arrow([], body, true))));
} else {
component_block.body.push(.../** @type {ESTree.Statement[]} */ (instance.body));
component_block.body.push(
...state.instance_level_snippets,
.../** @type {ESTree.Statement[]} */ (instance.body)
);
if (should_inject_context && component_returned_object.length > 0) {
component_block.body.push(b.var('$$exports', b.object(component_returned_object)));
}
component_block.body.unshift(...store_setup);
if (!analysis.runes && analysis.needs_context) {
component_block.body.push(b.stmt(b.call('$.init', analysis.immutable ? b.true : undefined)));
@ -392,12 +411,6 @@ export function client_component(analysis, options) {
);
}
const should_inject_context =
dev ||
analysis.needs_context ||
analysis.reactive_statements.size > 0 ||
component_returned_object.length > 0;
let should_inject_props =
should_inject_context ||
analysis.needs_props ||
@ -444,7 +457,7 @@ export function client_component(analysis, options) {
let to_push;
if (component_returned_object.length > 0) {
let pop_call = b.call('$.pop', b.object(component_returned_object));
let pop_call = b.call('$.pop', b.id('$$exports'));
to_push = needs_store_cleanup ? b.var('$$pop', pop_call) : b.return(pop_call);
} else {
to_push = b.stmt(b.call('$.pop'));
@ -455,6 +468,7 @@ export function client_component(analysis, options) {
if (needs_store_cleanup) {
component_block.body.push(b.stmt(b.call('$$cleanup')));
if (component_returned_object.length > 0) {
component_block.body.push(b.return(b.id('$$pop')));
}

@ -100,7 +100,7 @@ export function call(callee, ...args) {
if (typeof callee === 'string') callee = id(callee);
args = args.slice();
// replacing missing arguments with `undefined`, unless they're at the end in which case remove them
// replacing missing arguments with `void(0)`, unless they're at the end in which case remove them
let i = args.length;
let popping = true;
while (i--) {
@ -108,7 +108,7 @@ export function call(callee, ...args) {
if (popping) {
args.pop();
} else {
args[i] = id('undefined');
args[i] = void0;
}
} else {
popping = false;

@ -1,10 +1,5 @@
/** @import { Effect, Source, TemplateNode, } from '#client' */
import {
BOUNDARY_EFFECT,
EFFECT_PRESERVED,
EFFECT_RAN,
EFFECT_TRANSPARENT
} from '#client/constants';
import { BOUNDARY_EFFECT, EFFECT_PRESERVED, EFFECT_TRANSPARENT } from '#client/constants';
import { component_context, set_component_context } from '../../context.js';
import { handle_error, invoke_error_boundary } from '../../error-handling.js';
import { block, branch, destroy_effect, pause_effect } from '../../reactivity/effects.js';

@ -238,10 +238,10 @@ export function set_custom_element_data(node, prop, value) {
// Don't compute setters for custom elements while they aren't registered yet,
// because during their upgrade/instantiation they might add more setters.
// Instead, fall back to a simple "an object, then set as property" heuristic.
(setters_cache.has(node.nodeName) ||
(setters_cache.has(node.getAttribute('is') || node.nodeName) ||
// customElements may not be available in browser extension contexts
!customElements ||
customElements.get(node.tagName.toLowerCase())
customElements.get(node.getAttribute('is') || node.tagName.toLowerCase())
? get_setters(node).includes(prop)
: value && typeof value === 'object')
) {
@ -546,9 +546,10 @@ var setters_cache = new Map();
/** @param {Element} element */
function get_setters(element) {
var setters = setters_cache.get(element.nodeName);
var cache_key = element.getAttribute('is') || element.nodeName;
var setters = setters_cache.get(cache_key);
if (setters) return setters;
setters_cache.set(element.nodeName, (setters = []));
setters_cache.set(cache_key, (setters = []));
var descriptors;
var proto = element; // In the case of custom elements there might be setters on the instance

@ -6,7 +6,7 @@ import * as e from '../../../errors.js';
import { is } from '../../../proxy.js';
import { queue_micro_task } from '../../task.js';
import { hydrating } from '../../hydration.js';
import { untrack } from '../../../runtime.js';
import { tick, untrack } from '../../../runtime.js';
import { is_runes } from '../../../context.js';
import { current_batch, previous_batch } from '../../../reactivity/batch.js';
@ -17,11 +17,9 @@ import { current_batch, previous_batch } from '../../../reactivity/batch.js';
* @returns {void}
*/
export function bind_value(input, get, set = get) {
var runes = is_runes();
var batches = new WeakSet();
listen_to_event_and_reset_event(input, 'input', (is_reset) => {
listen_to_event_and_reset_event(input, 'input', async (is_reset) => {
if (DEV && input.type === 'checkbox') {
// TODO should this happen in prod too?
e.bind_invalid_checkbox_value();
@ -36,9 +34,13 @@ export function bind_value(input, get, set = get) {
batches.add(current_batch);
}
// In runes mode, respect any validation in accessors (doesn't apply in legacy mode,
// because we use mutable state which ensures the render effect always runs)
if (runes && value !== (value = get())) {
// Because `{#each ...}` blocks work by updating sources inside the flush,
// we need to wait a tick before checking to see if we should forcibly
// update the input and reset the selection state
await tick();
// Respect any validation in accessors
if (value !== (value = get())) {
var start = input.selectionStart;
var end = input.selectionEnd;

@ -73,11 +73,13 @@ function capture() {
var previous_effect = active_effect;
var previous_reaction = active_reaction;
var previous_component_context = component_context;
var previous_batch = current_batch;
return function restore() {
set_active_effect(previous_effect);
set_active_reaction(previous_reaction);
set_component_context(previous_component_context);
previous_batch?.activate();
if (DEV) {
set_from_async_derived(null);
@ -176,8 +178,8 @@ export function unset_context() {
* @param {() => Promise<void>} fn
*/
export async function async_body(fn) {
const unsuspend = suspend();
const active = /** @type {Effect} */ (active_effect);
var unsuspend = suspend();
var active = /** @type {Effect} */ (active_effect);
try {
await fn();

@ -76,8 +76,8 @@ let queued_root_effects = [];
let last_scheduled_effect = null;
let is_flushing = false;
let is_flushing_sync = false;
export class Batch {
/**
* The current values of any sources that are updated in this batch
@ -187,7 +187,7 @@ export class Batch {
// if there are multiple batches, we are 'time travelling' —
// we need to undo the changes belonging to any batch
// other than the current one
if (batches.size > 1) {
if (async_mode_flag && batches.size > 1) {
current_values = new Map();
batch_deriveds = new Map();
@ -484,6 +484,7 @@ export class Batch {
*/
export function flushSync(fn) {
if (async_mode_flag && active_effect !== null) {
// We disallow this because it creates super-hard to reason about stack trace and because it's generally a bad idea
e.flush_sync_in_effect();
}
@ -622,7 +623,9 @@ function flush_queued_effects(effects) {
}
}
if (eager_block_effects.length > 0) {
// If update_effect() has a flushSync() in it, we may have flushed another flush_queued_effects(),
// which already handled this logic and did set eager_block_effects to null.
if (eager_block_effects?.length > 0) {
// TODO this feels incorrect! it gets the tests passing
old_values.clear();
@ -678,6 +681,8 @@ export function suspend() {
if (!pending) {
batch.activate();
batch.decrement();
} else {
batch.deactivate();
}
unset_context();

@ -120,6 +120,9 @@ export function async_derived(fn, location) {
try {
var p = fn();
// Make sure to always access the then property to read any signals
// it might access, so that we track them as dependencies.
if (prev) Promise.resolve(p).catch(() => {}); // avoid unhandled rejection
} catch (error) {
p = Promise.reject(error);
}

@ -4,5 +4,5 @@
* The current version, as set in package.json.
* @type {string}
*/
export const VERSION = '5.38.3';
export const VERSION = '5.38.6';
export const PUBLIC_VERSION = '5';

@ -0,0 +1,41 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target }) {
const [increment, pop] = target.querySelectorAll('button');
increment.click();
await tick();
pop.click();
await tick();
pop.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>increment</button>
<button>pop</button>
<p>1</p>
`
);
increment.click();
await tick();
pop.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>increment</button>
<button>pop</button>
<p>2</p>
`
);
}
});

@ -0,0 +1,35 @@
<script>
let count = $state(0);
let deferreds = [];
class X {
constructor(promise) {
this.promise = promise;
}
get then() {
count;
return (resolve) => this.promise.then(() => count).then(resolve)
}
}
function push() {
const deferred = Promise.withResolvers();
deferreds.push(deferred);
return new X(deferred.promise);
}
</script>
<button onclick={() => count += 1}>increment</button>
<button onclick={() => deferreds.pop()?.resolve(count)}>pop</button>
<svelte:boundary>
<p>{await push()}</p>
{#snippet pending()}
<p>loading...</p>
{/snippet}
</svelte:boundary>

@ -0,0 +1,7 @@
<script lang="ts">
import { resolve } from './main.svelte';
const bar = await new Promise((r) => resolve.push(() => r('bar')));
</script>
<p>bar: {bar}</p>

@ -0,0 +1,10 @@
<script lang="ts">
import { resolve } from './main.svelte';
import Bar from './Bar.svelte';
const foo = await new Promise((r) => resolve.push(() => r('foo')));
</script>
<p>foo: {foo}</p>
<Bar/>

@ -0,0 +1,42 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target }) {
const [show, resolve] = target.querySelectorAll('button');
show.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>show</button>
<button>resolve</button>
<p>pending...</p>
`
);
resolve.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>show</button>
<button>resolve</button>
<p>pending...</p>
`
);
resolve.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>show</button>
<button>resolve</button>
<p>foo: foo</p>
<p>bar: bar</p>
`
);
}
});

@ -0,0 +1,31 @@
<script module>
export let resolve = [];
</script>
<script>
import Foo from './Foo.svelte';
let show = $state(false);
</script>
<button onclick={() => show = true}>
show
</button>
<button onclick={() => resolve.shift()()}>
resolve
</button>
<svelte:boundary>
{#if show}
<Foo/>
{/if}
{#if $effect.pending()}
<p>pending...</p>
{/if}
{#snippet pending()}
<p>initializing...</p>
{/snippet}
</svelte:boundary>

@ -29,6 +29,7 @@ export default test({
<button>c</button>
<button>ok</button>
<p>c</p>
<p>b or c</p>
`
);
@ -46,6 +47,7 @@ export default test({
<button>c</button>
<button>ok</button>
<p>b</p>
<p>b or c</p>
`
);
}

@ -33,6 +33,10 @@
<p>c</p>
{/if}
{#if route === 'b' || route === 'c'}
<p>b or c</p>
{/if}
{#snippet pending()}
<p>pending...</p>
{/snippet}

@ -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, 'value');
}
});

@ -0,0 +1,9 @@
<script>
const value = await 'value';
</script>
{#snippet valueSnippet()}
{value}
{/snippet}
{@render valueSnippet()}

@ -0,0 +1,8 @@
<script>
import App from './app.svelte';
</script>
<svelte:boundary>
{#snippet pending()}
{/snippet}
<App />
</svelte:boundary>

@ -0,0 +1,8 @@
<script lang="ts">
import { resolve } from './main.svelte';
const foo = $derived(await new Promise((r) => resolve.push(() => r('foo'))));
const bar = $derived(await new Promise((r) => resolve.push(() => r('bar'))));
</script>
<p>{foo} {bar}</p>

@ -0,0 +1,41 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target }) {
const [show, resolve] = target.querySelectorAll('button');
show.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>show</button>
<button>resolve</button>
<p>pending...</p>
`
);
resolve.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>show</button>
<button>resolve</button>
<p>pending...</p>
`
);
resolve.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>show</button>
<button>resolve</button>
<p>foo bar</p>
`
);
}
});

@ -0,0 +1,31 @@
<script module>
export let resolve = [];
</script>
<script>
import Foo from './Foo.svelte';
let show = $state(false);
</script>
<button onclick={() => show = true}>
show
</button>
<button onclick={() => resolve.shift()()}>
resolve
</button>
<svelte:boundary>
{#if show}
<Foo/>
{/if}
{#if $effect.pending()}
<p>pending...</p>
{/if}
{#snippet pending()}
<p>initializing...</p>
{/snippet}
</svelte:boundary>

@ -0,0 +1,28 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
mode: ['client', 'hydrate'],
html: `<input><p>a</a>`,
async test({ assert, target }) {
const [input] = target.querySelectorAll('input');
input.focus();
input.value = 'ab';
input.dispatchEvent(new InputEvent('input', { bubbles: true }));
flushSync();
assert.htmlEqual(target.innerHTML, `<input><p>ab</a>`);
assert.equal(input.value, 'ab');
input.focus();
input.value = 'abc';
input.dispatchEvent(new InputEvent('input', { bubbles: true }));
flushSync();
assert.htmlEqual(target.innerHTML, `<input><p>abc</a>`);
assert.equal(input.value, 'abc');
}
});

@ -0,0 +1,8 @@
<script>
let array = $state([{ value: 'a' }]);
</script>
{#each array as obj}
<input bind:value={() => obj.value, (value) => array = [{ value }]} />
<p>{obj.value}</p>
{/each}

@ -0,0 +1,30 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
mode: ['client', 'hydrate'],
async test({ assert, target }) {
const [input] = target.querySelectorAll('input');
input.focus();
input.value = 'Ab';
input.dispatchEvent(new InputEvent('input', { bubbles: true }));
await tick();
await tick();
assert.equal(input.value, 'AB');
assert.htmlEqual(target.innerHTML, `<input /><p>AB</p>`);
input.focus();
input.value = 'ABc';
input.dispatchEvent(new InputEvent('input', { bubbles: true }));
await tick();
await tick();
assert.equal(input.value, 'ABC');
assert.htmlEqual(target.innerHTML, `<input /><p>ABC</p>`);
}
});

@ -0,0 +1,6 @@
<script>
let text = $state('A');
</script>
<input bind:value={() => text, (v) => text = v.toUpperCase()} />
<p>{text}</p>

@ -20,5 +20,8 @@ export default test({
const [value1, value2] = target.querySelectorAll('value-element');
assert.equal(value1.shadowRoot?.innerHTML, '<span>test</span>');
assert.equal(value2.shadowRoot?.innerHTML, '<span>test</span>');
const value_builtin = target.querySelector('div');
assert.equal(value_builtin?.shadowRoot?.innerHTML, '<span>test</span>');
}
});

@ -15,6 +15,24 @@
}
});
}
if(!customElements.get('value-builtin')) {
customElements.define('value-builtin', class extends HTMLDivElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
}
set value(v) {
if (this.__value !== v) {
this.__value = v;
this.shadowRoot.innerHTML = `<span>${v}</span>`;
}
}
}, {
extends: 'div'
});
}
</script>
<my-element string="test" object={{ test: true }}></my-element>
@ -22,3 +40,4 @@
<value-element value="test"></value-element>
<value-element {...{value: "test"}}></value-element>
<div is="value-builtin" value="test"></div>

@ -0,0 +1,7 @@
<script>
let { text } = $props();
$effect(() => console.log(text));
</script>
{text}

@ -0,0 +1,12 @@
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, `<button>show</button> <div>hello</div>`);
assert.deepEqual(logs, ['hello']);
},
runtime_error: async_mode ? 'flush_sync_in_effect' : undefined
});

@ -0,0 +1,13 @@
<script>
import { flushSync, mount } from 'svelte'
import Child from './Child.svelte';
let show = $state(false);
</script>
<button onclick={() => show = true}>show</button>
<div {@attach (target) => {
mount(Child, { target, props: { text: 'hello' } });
flushSync();
}}></div>
Loading…
Cancel
Save