diff --git a/.changeset/cool-garlics-fail.md b/.changeset/cool-garlics-fail.md deleted file mode 100644 index cabff1840d..0000000000 --- a/.changeset/cool-garlics-fail.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: place instance-level snippets inside async body diff --git a/.changeset/silent-pigs-relax.md b/.changeset/silent-pigs-relax.md deleted file mode 100644 index 5acf185ffe..0000000000 --- a/.changeset/silent-pigs-relax.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: restore batch along with effect context diff --git a/documentation/docs/07-misc/04-custom-elements.md b/documentation/docs/07-misc/04-custom-elements.md index 7e6a17b947..4e5afff7d2 100644 --- a/documentation/docs/07-misc/04-custom-elements.md +++ b/documentation/docs/07-misc/04-custom-elements.md @@ -4,7 +4,7 @@ title: Custom elements -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 `` [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 `` [element](svelte-options). Within the custom element you can access the host element via the [`$host`](https://svelte.dev/docs/svelte/$host) rune. ```svelte diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index fb6b20c489..de94eb1897 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -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 diff --git a/packages/svelte/elements.d.ts b/packages/svelte/elements.d.ts index f63a31a96b..b0c2fae2de 100644 --- a/packages/svelte/elements.d.ts +++ b/packages/svelte/elements.d.ts @@ -1268,6 +1268,7 @@ export interface HTMLMetaAttributes extends HTMLAttributes { charset?: string | undefined | null; content?: string | undefined | null; 'http-equiv'?: + | 'accept-ch' | 'content-security-policy' | 'content-type' | 'default-style' diff --git a/packages/svelte/package.json b/packages/svelte/package.json index b7effe35bd..fe42603184 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -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": { diff --git a/packages/svelte/src/internal/client/dom/elements/attributes.js b/packages/svelte/src/internal/client/dom/elements/attributes.js index 2fa5d4541c..a5b7140f25 100644 --- a/packages/svelte/src/internal/client/dom/elements/attributes.js +++ b/packages/svelte/src/internal/client/dom/elements/attributes.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 diff --git a/packages/svelte/src/internal/client/dom/elements/bindings/input.js b/packages/svelte/src/internal/client/dom/elements/bindings/input.js index 67e6ff1dd2..815acde7c5 100644 --- a/packages/svelte/src/internal/client/dom/elements/bindings/input.js +++ b/packages/svelte/src/internal/client/dom/elements/bindings/input.js @@ -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; diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index 7933401496..9ac9987f21 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -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(); diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index be70e358a2..f0037f6405 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -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); } diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index 815b25bf1f..67c586790f 100644 --- a/packages/svelte/src/version.js +++ b/packages/svelte/src/version.js @@ -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'; diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-reverse-order/_config.js b/packages/svelte/tests/runtime-runes/samples/async-derived-reverse-order/_config.js new file mode 100644 index 0000000000..bd0dd753c2 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-derived-reverse-order/_config.js @@ -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, + ` + + +

1

+ ` + ); + + increment.click(); + await tick(); + + pop.click(); + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` + + +

2

+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-reverse-order/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-derived-reverse-order/main.svelte new file mode 100644 index 0000000000..b9f6c26c2a --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-derived-reverse-order/main.svelte @@ -0,0 +1,35 @@ + + + + + + + +

{await push()}

+ + {#snippet pending()} +

loading...

+ {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/binding-update-in-each/_config.js b/packages/svelte/tests/runtime-runes/samples/binding-update-in-each/_config.js new file mode 100644 index 0000000000..b6371ce11c --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/binding-update-in-each/_config.js @@ -0,0 +1,28 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + mode: ['client', 'hydrate'], + + html: `

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

ab`); + assert.equal(input.value, 'ab'); + + input.focus(); + input.value = 'abc'; + input.dispatchEvent(new InputEvent('input', { bubbles: true })); + flushSync(); + + assert.htmlEqual(target.innerHTML, `

abc`); + assert.equal(input.value, 'abc'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/binding-update-in-each/main.svelte b/packages/svelte/tests/runtime-runes/samples/binding-update-in-each/main.svelte new file mode 100644 index 0000000000..7925195ee1 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/binding-update-in-each/main.svelte @@ -0,0 +1,8 @@ + + +{#each array as obj} + obj.value, (value) => array = [{ value }]} /> +

{obj.value}

+{/each} diff --git a/packages/svelte/tests/runtime-runes/samples/binding-update-while-focused-3/_config.js b/packages/svelte/tests/runtime-runes/samples/binding-update-while-focused-3/_config.js new file mode 100644 index 0000000000..0909dee7a8 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/binding-update-while-focused-3/_config.js @@ -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, `

AB

`); + + 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, `

ABC

`); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/binding-update-while-focused-3/main.svelte b/packages/svelte/tests/runtime-runes/samples/binding-update-while-focused-3/main.svelte new file mode 100644 index 0000000000..b61bfe4e67 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/binding-update-while-focused-3/main.svelte @@ -0,0 +1,6 @@ + + + text, (v) => text = v.toUpperCase()} /> +

{text}

diff --git a/packages/svelte/tests/runtime-runes/samples/custom-element-attributes/_config.js b/packages/svelte/tests/runtime-runes/samples/custom-element-attributes/_config.js index 7f406d8f0d..3d8917c147 100644 --- a/packages/svelte/tests/runtime-runes/samples/custom-element-attributes/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/custom-element-attributes/_config.js @@ -20,5 +20,8 @@ export default test({ const [value1, value2] = target.querySelectorAll('value-element'); assert.equal(value1.shadowRoot?.innerHTML, 'test'); assert.equal(value2.shadowRoot?.innerHTML, 'test'); + + const value_builtin = target.querySelector('div'); + assert.equal(value_builtin?.shadowRoot?.innerHTML, 'test'); } }); diff --git a/packages/svelte/tests/runtime-runes/samples/custom-element-attributes/main.svelte b/packages/svelte/tests/runtime-runes/samples/custom-element-attributes/main.svelte index 82774f160d..badb8f96c7 100644 --- a/packages/svelte/tests/runtime-runes/samples/custom-element-attributes/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/custom-element-attributes/main.svelte @@ -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 = `${v}`; + } + } + }, { + extends: 'div' + }); + } @@ -22,3 +40,4 @@ +
diff --git a/packages/svelte/tests/runtime-runes/samples/flush-sync-inside-attachment/Child.svelte b/packages/svelte/tests/runtime-runes/samples/flush-sync-inside-attachment/Child.svelte new file mode 100644 index 0000000000..44447e4f36 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/flush-sync-inside-attachment/Child.svelte @@ -0,0 +1,7 @@ + + +{text} diff --git a/packages/svelte/tests/runtime-runes/samples/flush-sync-inside-attachment/_config.js b/packages/svelte/tests/runtime-runes/samples/flush-sync-inside-attachment/_config.js new file mode 100644 index 0000000000..ec8858b2c6 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/flush-sync-inside-attachment/_config.js @@ -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, `
hello
`); + assert.deepEqual(logs, ['hello']); + }, + runtime_error: async_mode ? 'flush_sync_in_effect' : undefined +}); diff --git a/packages/svelte/tests/runtime-runes/samples/flush-sync-inside-attachment/main.svelte b/packages/svelte/tests/runtime-runes/samples/flush-sync-inside-attachment/main.svelte new file mode 100644 index 0000000000..bef820376b --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/flush-sync-inside-attachment/main.svelte @@ -0,0 +1,13 @@ + + + + +
{ + mount(Child, { target, props: { text: 'hello' } }); + flushSync(); +}}>