Merge branch 'main' into async-another-try

async-another-try
Simon Holthausen 2 days ago
commit 894f376b37
No known key found for this signature in database

@ -1,5 +0,0 @@
---
'svelte': patch
---
fix: cancel deferred event listeners during cleanup

@ -1,5 +0,0 @@
---
'svelte': patch
---
fix: preserve global CSS in components without scopable elements

@ -1,5 +0,0 @@
---
'svelte': patch
---
fix: reduce SSR render result garbage collection

@ -1,5 +0,0 @@
---
'svelte': patch
---
fix: resolve the fallback of an each block in the enclosing scope

@ -1,5 +0,0 @@
---
'svelte': patch
---
perf: speed up parser interactions with Acorn or avoid them where possible

@ -1,5 +0,0 @@
---
'svelte': patch
---
fix: prevent effect tree of batches from interfering with each other

@ -1,5 +0,0 @@
---
'svelte': patch
---
fix: serialize input default values during server rendering

@ -1,5 +0,0 @@
---
'svelte': patch
---
fix: remove `WAS_MARKED` flag in favor of `Set`

@ -1,5 +0,0 @@
---
'svelte': patch
---
fix: throw `set_context_after_init` when `setContext` is called after an `await` during SSR

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: don't overwrite an unchanged spread `value`, preserving incomplete number input

@ -1,5 +0,0 @@
---
'svelte': patch
---
fix: keep `$state.eager` when used as a variable initializer

@ -1,5 +0,0 @@
---
'svelte': patch
---
perf: avoid regex matching in parser where possible

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: prevent hydration mismatch recovery from being intercepted by error boundaries

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: preserve dynamic element connections during hydration

@ -1,5 +0,0 @@
---
'svelte': patch
---
fix: in non-async mode, only push variable to current_sources when active_reaction is updating

@ -1,5 +0,0 @@
---
'svelte': patch
---
fix: recognise `aria-braillelabel` and `aria-brailleroledescription` as known ARIA attributes

@ -1,5 +1,37 @@
# svelte
## 5.57.1
### Patch Changes
- fix: cancel deferred event listeners during cleanup ([#18749](https://github.com/sveltejs/svelte/pull/18749))
- fix: preserve global CSS in components without scopable elements ([#18793](https://github.com/sveltejs/svelte/pull/18793))
- fix: reduce SSR render result garbage collection ([#18798](https://github.com/sveltejs/svelte/pull/18798))
- fix: resolve the fallback of an each block in the enclosing scope ([#18803](https://github.com/sveltejs/svelte/pull/18803))
- perf: speed up parser interactions with Acorn or avoid them where possible ([#18740](https://github.com/sveltejs/svelte/pull/18740))
- fix: prevent effect tree of batches from interfering with each other ([#18508](https://github.com/sveltejs/svelte/pull/18508))
- fix: serialize input default values during server rendering ([#18733](https://github.com/sveltejs/svelte/pull/18733))
- fix: remove `WAS_MARKED` flag in favor of `Set` ([#18127](https://github.com/sveltejs/svelte/pull/18127))
- fix: throw `set_context_after_init` when `setContext` is called after an `await` during SSR ([#18739](https://github.com/sveltejs/svelte/pull/18739))
- fix: make Object.hasOwn reactive for state proxy ownership changes ([#18838](https://github.com/sveltejs/svelte/pull/18838))
- fix: keep `$state.eager` when used as a variable initializer ([#18809](https://github.com/sveltejs/svelte/pull/18809))
- perf: avoid regex matching in parser where possible ([#18736](https://github.com/sveltejs/svelte/pull/18736))
- fix: in non-async mode, only push variable to current_sources when active_reaction is updating ([#18550](https://github.com/sveltejs/svelte/pull/18550))
- fix: recognise `aria-braillelabel` and `aria-brailleroledescription` as known ARIA attributes ([#18765](https://github.com/sveltejs/svelte/pull/18765))
## 5.57.0
### Minor Changes

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

@ -146,16 +146,9 @@ export function prune(stylesheet, elements) {
ComplexSelector(node) {
const selectors = get_relative_selectors(node);
const rule = /** @type {Compiler.AST.CSS.Rule} */ (node.metadata.rule);
const first = selectors[0]?.selectors[0];
const is_icss_export =
selectors.length === 1 &&
selectors[0].selectors.length === 1 &&
first?.type === 'PseudoClassSelector' &&
first.name === 'export' &&
first.args === null;
// Global and ICSS export rules do not depend on an element in this component
if (every_is_global(selectors, 0, selectors.length, rule) || is_icss_export) {
if (every_is_global(selectors, 0, selectors.length, rule)) {
node.metadata.used = true;
}

@ -1,6 +1,10 @@
/** @import { Effect, Reaction, Source, TemplateNode, } from '#client' */
import { BOUNDARY_EFFECT, EFFECT_PRESERVED, EFFECT_TRANSPARENT } from '#client/constants';
import { HYDRATION_START_ELSE, HYDRATION_START_FAILED } from '../../../../constants.js';
import {
HYDRATION_ERROR,
HYDRATION_START_ELSE,
HYDRATION_START_FAILED
} from '../../../../constants.js';
import { component_context, set_component_context } from '../../context.js';
import { invoke_error_boundary } from '../../error-handling.js';
import {
@ -442,6 +446,10 @@ export class Boundary {
/** @param {unknown} error */
error(error) {
if (error === HYDRATION_ERROR) {
throw error;
}
// If we have nothing to capture the error, or if we hit an error while
// rendering the fallback, re-throw for another boundary to handle
if (!this.#props.onerror && !this.#props.failed) {

@ -1,4 +1,4 @@
/** @import { EachItem, EachOutroGroup, EachState, Effect, EffectNodes, MaybeSource, Source, TemplateNode, TransitionManager, Value } from '#client' */
/** @import { EachItem, EachOutroGroup, EachState, Effect, EffectNodes, MaybeSource, TemplateNode, TransitionManager } from '#client' */
/** @import { Batch } from '../../reactivity/batch.js'; */
import {
EACH_INDEX_REACTIVE,

@ -71,6 +71,7 @@ export function element(node, get_tag, is_svg, render_fn, get_namespace, locatio
branches.ensure(next_tag, (anchor) => {
if (next_tag) {
var is_hydrating = hydrating;
element = hydrating ? /** @type {Element} */ (element) : create_element(next_tag, ns);
if (DEV && location) {
@ -123,7 +124,8 @@ export function element(node, get_tag, is_svg, render_fn, get_namespace, locatio
// we do this after calling `render_fn` so that child effects don't override `nodes.end`
/** @type {Effect & { nodes: EffectNodes }} */ (active_effect).nodes.end = element;
anchor.before(element);
// we only move the node if we are not hydrating since a claimed element is already in place
if (!is_hydrating) anchor.before(element);
}
if (hydrating) {

@ -422,7 +422,21 @@ function set_attributes(
} else if (!is_custom_element && (key === '__value' || (key === 'value' && value != null))) {
// @ts-ignore We're not running this for custom elements because __value is actually
// how Lit stores the current value on the element, and messing with that would break things.
element.value = element.__value = value;
element.__value = value;
// we don't set the value if it hasn't changed. This supports invalid number inputs like `1e` because
// 1. user types 1e
// 2. the state is updated reading e.target.value which is ''
// 3. the spreaded value is ''
// 4. updating input.value would thus, clear the user value
if (
prev_value == null ||
// @ts-ignore
element.value !== value ||
(value === 0 && element.nodeName === PROGRESS_TAG)
) {
// @ts-ignore
element.value = value;
}
} else if (key === 'selected' && is_option_element) {
set_selected(/** @type {HTMLOptionElement} */ (element), value);
} else {

@ -1,7 +1,7 @@
/** @import { Derived, Effect } from '#client' */
/** @import { Boundary } from './dom/blocks/boundary.js' */
import { DEV } from 'esm-env';
import { FILENAME } from '../../constants.js';
import { FILENAME, HYDRATION_ERROR } from '../../constants.js';
import { is_firefox } from './dom/operations.js';
import {
ERROR_VALUE,
@ -52,6 +52,10 @@ export function handle_error(error) {
* @param {Effect | null} effect
*/
export function invoke_error_boundary(error, effect) {
if (error === HYDRATION_ERROR) {
throw error;
}
if (effect !== null && (effect.f & DESTROYED) !== 0) {
return;
}

@ -204,16 +204,21 @@ export function proxy(value) {
},
getOwnPropertyDescriptor(target, prop) {
this.has?.(target, prop);
var descriptor = Reflect.getOwnPropertyDescriptor(target, prop);
var s = sources.get(prop);
if (descriptor && 'value' in descriptor) {
var s = sources.get(prop);
if (s) descriptor.value = get(s);
} else if (descriptor === undefined) {
var source = sources.get(prop);
var value = source?.v;
if (s !== undefined) {
var value = get(s);
if (value === UNINITIALIZED) {
return undefined;
}
if (source !== undefined && value !== UNINITIALIZED) {
if (descriptor && 'value' in descriptor) {
descriptor.value = value;
} else {
return {
enumerable: true,
configurable: true,

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

@ -1,2 +1 @@
:export { foo: red; }
:is(td, th) { color: red; }

@ -1,6 +1,5 @@
<svelte:head><meta name="x" content="y" /></svelte:head>
<style>
:export { foo: red; }
:is(:global(td), :global(th)) { color: red; }
</style>

@ -0,0 +1,5 @@
import { test } from '../../test';
export default test({
expect_hydration_error: true
});

@ -0,0 +1 @@
<!----><p><p>Valid HTML fragment</p><!----></p><!---->

@ -0,0 +1,7 @@
<svelte:boundary>
<p>{@html '<p>Valid HTML fragment</p>'}</p>
{#snippet failed()}
<p>boundary fallback</p>
{/snippet}
</svelte:boundary>

@ -0,0 +1,10 @@
import { test } from '../../test';
export default test({
server_props: { condition: false },
props: { condition: true },
snapshot(target) {
return { element: target.querySelector('div'), sibling: target.querySelector(':scope > p') };
}
});

@ -0,0 +1,9 @@
<script>
export let condition;
export let tag = 'div';
</script>
<svelte:element this={tag}>
{#if condition}<p>client</p>{:else}<span>server</span>{/if}
</svelte:element>
<p>after</p>

@ -128,6 +128,7 @@ function normalize_children(node) {
* id_prefix?: string;
* props?: Props;
* compileOptions?: Partial<CompileOptions>;
* before_test?: () => void;
* test?: (args: {
* assert: typeof assert & {
* htmlEqual(a: string, b: string, description?: string): void;

@ -0,0 +1,71 @@
import { flushSync } from 'svelte';
import { assert_ok, test } from '../../assert';
/** @type {Record<string, number>} */
const connections = {};
/** @type {string[]} */
const disconnections = [];
/** @type {Element[]} */
let claimed;
/** @type {MutationObserver} */
let observer;
export default test({
before_test() {
const target = document.querySelector('main');
assert_ok(target);
claimed = Array.from(target.children);
customElements.define(
'connection-probe',
class extends HTMLElement {
connectedCallback() {
connections[this.id] = (connections[this.id] || 0) + 1;
}
disconnectedCallback() {
disconnections.push(this.id);
}
}
);
observer = new MutationObserver(() => {});
observer.observe(target, { childList: true });
},
test({ assert, component, target }) {
const removed = observer.takeRecords().flatMap((record) => Array.from(record.removedNodes));
observer.disconnect();
assert.deepEqual(
removed
.filter((node) => node instanceof Element)
.filter((node) => claimed.includes(node))
.map((node) => node.id),
[]
);
assert.deepEqual(connections, { child: 1, custom: 1 });
assert.deepEqual(disconnections, []);
flushSync(() => {
component.tag = 'section';
component.empty_tag = 'span';
component.void_tag = 'hr';
component.custom_tag = 'aside';
});
assert.equal(target.querySelector('#parent')?.tagName, 'SECTION');
assert.equal(target.querySelector('#empty')?.tagName, 'SPAN');
assert.equal(target.querySelector('#void')?.tagName, 'HR');
assert.equal(target.querySelector('#custom')?.tagName, 'ASIDE');
assert.deepEqual(connections, { child: 2, custom: 1 });
assert.deepEqual(disconnections, ['child', 'custom']);
flushSync(() => {
component.tag = null;
});
assert.equal(target.querySelector('#parent'), null);
assert.deepEqual(disconnections, ['child', 'custom', 'child']);
flushSync(() => {
component.tag = 'div';
});
assert.equal(target.querySelector('#parent')?.tagName, 'DIV');
assert.deepEqual(connections, { child: 3, custom: 1 });
}
});

@ -0,0 +1,11 @@
<script>
export let tag = 'div';
export let empty_tag = 'div';
export let void_tag = 'input';
export let custom_tag = 'connection-probe';
</script>
<svelte:element this={tag} id="parent"><connection-probe id="child"></connection-probe></svelte:element>
<svelte:element this={empty_tag} id="empty" />
<svelte:element this={void_tag} id="void" />
<svelte:element this={custom_tag} id="custom" />

@ -0,0 +1,28 @@
import { flushSync } from 'svelte';
import { ok, test } from '../../assert';
export default test({
async test({ assert, target }) {
const input = target.querySelector('input');
ok(input);
input.focus();
// we need to use `document.execCommand('insertText', false, ...)` to simulate user input
// because directly setting an invalid value to `input.value` would simply clear the input
// and dispatching an event would not update the input correctly
document.execCommand('insertText', false, '1');
flushSync();
// `1e` is incomplete on every platform, unlike `1.` which Chromium on Linux accepts as `1`
document.execCommand('insertText', false, 'e');
flushSync();
assert.equal(input.value, '');
assert.equal(input.validity.badInput, true);
document.execCommand('insertText', false, '5');
flushSync();
assert.equal(input.value, '1e5');
}
});

@ -0,0 +1,5 @@
<script>
let value = $state('');
</script>
<input {...{ type: 'number', value }} oninput={(e) => (value = e.currentTarget.value)} />

@ -0,0 +1,24 @@
import { test } from '../../test';
import { tick } from 'svelte';
export default test({
html: `<button>add y</button><button>delete y</button><p>false</p>`,
async test({ assert, target }) {
const [add, remove] = target.querySelectorAll('button');
add.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`<button>add y</button><button>delete y</button><p>true</p>`
);
remove.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`<button>add y</button><button>delete y</button><p>false</p>`
);
}
});

@ -0,0 +1,7 @@
<script>
let state = $state({});
</script>
<button onclick={() => (state.y = true)}>add y</button>
<button onclick={() => delete state.y}>delete y</button>
<p>{Object.hasOwn(state, 'y')}</p>
Loading…
Cancel
Save