Merge branch 'main' into boundary-batch-nullpointer-fix

boundary-batch-nullpointer-fix
Rich Harris 4 days ago
commit dce25638c4

@ -1,5 +0,0 @@
---
'svelte': patch
---
fix: place instance-level snippets inside async body

@ -1,5 +0,0 @@
---
'svelte': patch
---
fix: restore batch along with effect context

@ -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": {

@ -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;

@ -190,7 +190,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();
@ -525,6 +525,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();
}
@ -663,7 +664,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();

@ -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,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