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

boundary-batch-nullpointer-fix
Rich Harris 6 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) --> <!-- - [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
<svelte:options customElement="my-element" /> <svelte:options customElement="my-element" />

@ -1,5 +1,31 @@
# svelte # 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 ## 5.38.3
### Patch Changes ### Patch Changes

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

@ -2,7 +2,7 @@
"name": "svelte", "name": "svelte",
"description": "Cybernetically enhanced web apps", "description": "Cybernetically enhanced web apps",
"license": "MIT", "license": "MIT",
"version": "5.38.3", "version": "5.38.6",
"type": "module", "type": "module",
"types": "./types/index.d.ts", "types": "./types/index.d.ts",
"engines": { "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, // Don't compute setters for custom elements while they aren't registered yet,
// because during their upgrade/instantiation they might add more setters. // because during their upgrade/instantiation they might add more setters.
// Instead, fall back to a simple "an object, then set as property" heuristic. // 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 may not be available in browser extension contexts
!customElements || !customElements ||
customElements.get(node.tagName.toLowerCase()) customElements.get(node.getAttribute('is') || node.tagName.toLowerCase())
? get_setters(node).includes(prop) ? get_setters(node).includes(prop)
: value && typeof value === 'object') : value && typeof value === 'object')
) { ) {
@ -546,9 +546,10 @@ var setters_cache = new Map();
/** @param {Element} element */ /** @param {Element} element */
function get_setters(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; if (setters) return setters;
setters_cache.set(element.nodeName, (setters = [])); setters_cache.set(cache_key, (setters = []));
var descriptors; var descriptors;
var proto = element; // In the case of custom elements there might be setters on the instance 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 { is } from '../../../proxy.js';
import { queue_micro_task } from '../../task.js'; import { queue_micro_task } from '../../task.js';
import { hydrating } from '../../hydration.js'; import { hydrating } from '../../hydration.js';
import { untrack } from '../../../runtime.js'; import { tick, untrack } from '../../../runtime.js';
import { is_runes } from '../../../context.js'; import { is_runes } from '../../../context.js';
import { current_batch, previous_batch } from '../../../reactivity/batch.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} * @returns {void}
*/ */
export function bind_value(input, get, set = get) { export function bind_value(input, get, set = get) {
var runes = is_runes();
var batches = new WeakSet(); 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') { if (DEV && input.type === 'checkbox') {
// TODO should this happen in prod too? // TODO should this happen in prod too?
e.bind_invalid_checkbox_value(); e.bind_invalid_checkbox_value();
@ -36,9 +34,13 @@ export function bind_value(input, get, set = get) {
batches.add(current_batch); batches.add(current_batch);
} }
// In runes mode, respect any validation in accessors (doesn't apply in legacy mode, // Because `{#each ...}` blocks work by updating sources inside the flush,
// because we use mutable state which ensures the render effect always runs) // we need to wait a tick before checking to see if we should forcibly
if (runes && value !== (value = get())) { // update the input and reset the selection state
await tick();
// Respect any validation in accessors
if (value !== (value = get())) {
var start = input.selectionStart; var start = input.selectionStart;
var end = input.selectionEnd; var end = input.selectionEnd;

@ -190,7 +190,7 @@ export class Batch {
// if there are multiple batches, we are 'time travelling' — // if there are multiple batches, we are 'time travelling' —
// we need to undo the changes belonging to any batch // we need to undo the changes belonging to any batch
// other than the current one // other than the current one
if (batches.size > 1) { if (async_mode_flag && batches.size > 1) {
current_values = new Map(); current_values = new Map();
batch_deriveds = new Map(); batch_deriveds = new Map();
@ -525,6 +525,7 @@ export class Batch {
*/ */
export function flushSync(fn) { export function flushSync(fn) {
if (async_mode_flag && active_effect !== null) { 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(); 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 // TODO this feels incorrect! it gets the tests passing
old_values.clear(); old_values.clear();

@ -120,6 +120,9 @@ export function async_derived(fn, location) {
try { try {
var p = fn(); 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) { } catch (error) {
p = Promise.reject(error); p = Promise.reject(error);
} }

@ -4,5 +4,5 @@
* The current version, as set in package.json. * The current version, as set in package.json.
* @type {string} * @type {string}
*/ */
export const VERSION = '5.38.3'; export const VERSION = '5.38.6';
export const PUBLIC_VERSION = '5'; 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'); const [value1, value2] = target.querySelectorAll('value-element');
assert.equal(value1.shadowRoot?.innerHTML, '<span>test</span>'); assert.equal(value1.shadowRoot?.innerHTML, '<span>test</span>');
assert.equal(value2.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> </script>
<my-element string="test" object={{ test: true }}></my-element> <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>
<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