Merge branch 'main' into style-object-syntax

pull/18176/head
Mathias Picker 4 months ago committed by GitHub
commit da7b7a3198
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -1,5 +0,0 @@
---
'svelte': patch
---
fix: don't rebase just-created batches

@ -1,5 +0,0 @@
---
'svelte': patch
---
fix: flush eager effects in production

@ -1,5 +0,0 @@
---
'svelte': patch
---
fix: account for proxified instance when updating `bind:this`

@ -1,5 +0,0 @@
---
'svelte': patch
---
fix: ensure scheduled batch is flushed if not obsolete

@ -1,5 +0,0 @@
---
'svelte': patch
---
fix: resolve stale deriveds with latest value

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: inline primitive constants in attribute values during SSR

@ -1,5 +0,0 @@
---
'svelte': patch
---
fix: allow `@debug` tags to reference awaited variables

@ -1,5 +0,0 @@
---
'svelte': patch
---
fix: re-run fallback props if dependencies update

@ -1,5 +0,0 @@
---
'svelte': patch
---
fix: ignore comments when reading CSS values

@ -1,5 +0,0 @@
---
'svelte': patch
---
fix: ignore false-positive errors of `$inspect` dependencies

@ -163,6 +163,8 @@ export const [getCounter, setCounter] = createContext<Counter>();
Svelte will warn you if you get it wrong.
Similarly, to pass primitive values through context, use functions as described in [Passing state into functions]($state#Passing-state-into-functions).
## Component testing
When writing [component tests](testing#Unit-and-component-tests-with-Vitest-Component-testing), it can be useful to create a wrapper component that sets the context in order to check the behaviour of a component that uses it. As of version 5.49, you can do this sort of thing:

@ -1,5 +1,77 @@
# svelte
## 5.55.8
### Patch Changes
- fix(print): handle `svelte:body` and fix keyframe percentage double-printing ([#18234](https://github.com/sveltejs/svelte/pull/18234))
- fix: execute uninitialized derived even if it's destroyed ([#18228](https://github.com/sveltejs/svelte/pull/18228))
- fix: use named symbols everywhere ([#18238](https://github.com/sveltejs/svelte/pull/18238))
- fix: don't run teardown effects when deriveds are unfreezed ([#18227](https://github.com/sveltejs/svelte/pull/18227))
- fix: unset context synchronously in `run` ([#18236](https://github.com/sveltejs/svelte/pull/18236))
## 5.55.7
### Patch Changes
- fix: prevent XSS on `hydratable` from user contents ([`a16ebc67bbcf8f708360195687e1b2719463e1a4`](https://github.com/sveltejs/svelte/commit/a16ebc67bbcf8f708360195687e1b2719463e1a4))
- chore: bump devalue ([#18219](https://github.com/sveltejs/svelte/pull/18219))
- fix: disallow empty attribute names during SSR ([`547853e2406a2147ad7fb5ffeba95b01bd9642da`](https://github.com/sveltejs/svelte/commit/547853e2406a2147ad7fb5ffeba95b01bd9642da))
- fix: harden regex ([`d2375e2ebcab5c88feb5652f1a9d621b8f06b259`](https://github.com/sveltejs/svelte/commit/d2375e2ebcab5c88feb5652f1a9d621b8f06b259))
- fix: move Svelte runtime properties to symbols ([`e1cbbd96441e82c9eb8a23a2903c0d06d3cda991`](https://github.com/sveltejs/svelte/commit/e1cbbd96441e82c9eb8a23a2903c0d06d3cda991))
## 5.55.6
### Patch Changes
- fix: leave stale promises to wait for a later resolution, instead of rejecting ([#18180](https://github.com/sveltejs/svelte/pull/18180))
- fix: keep dependencies of `$state.eager/pending` ([#18218](https://github.com/sveltejs/svelte/pull/18218))
- fix: reapply context after transforming error during SSR ([#18099](https://github.com/sveltejs/svelte/pull/18099))
- fix: don't rebase just-created batches ([#18117](https://github.com/sveltejs/svelte/pull/18117))
- chore: allow `null` for `pending` in typings ([#18201](https://github.com/sveltejs/svelte/pull/18201))
- fix: flush eager effects in production ([#18107](https://github.com/sveltejs/svelte/pull/18107))
- fix: rethrow error of failed iterable after calling `return()` ([#18169](https://github.com/sveltejs/svelte/pull/18169))
- fix: account for proxified instance when updating `bind:this` ([#18147](https://github.com/sveltejs/svelte/pull/18147))
- fix: ensure scheduled batch is flushed if not obsolete ([#18131](https://github.com/sveltejs/svelte/pull/18131))
- fix: resolve stale deriveds with latest value ([#18167](https://github.com/sveltejs/svelte/pull/18167))
- chore: remove unnecessary `increment_pending` calls ([#18183](https://github.com/sveltejs/svelte/pull/18183))
- fix: correctly compile component member expressions for SSR ([#18192](https://github.com/sveltejs/svelte/pull/18192))
- fix: reset `source.updated` stack traces after `flush` ([#18196](https://github.com/sveltejs/svelte/pull/18196))
- fix: replacing async 'blocking' strategy with 'merging' ([#18205](https://github.com/sveltejs/svelte/pull/18205))
- fix: allow `@debug` tags to reference awaited variables ([#18138](https://github.com/sveltejs/svelte/pull/18138))
- fix: re-run fallback props if dependencies update ([#18146](https://github.com/sveltejs/svelte/pull/18146))
- fix: abort running obsolete async branches ([#18118](https://github.com/sveltejs/svelte/pull/18118))
- fix: ignore comments when reading CSS values ([#18153](https://github.com/sveltejs/svelte/pull/18153))
- fix: wrap `Promise.all` in `save` during SSR ([#18178](https://github.com/sveltejs/svelte/pull/18178))
- fix: ignore false-positive errors of `$inspect` dependencies ([#18106](https://github.com/sveltejs/svelte/pull/18106))
## 5.55.5
### Patch Changes

@ -2067,9 +2067,9 @@ export interface SvelteHTMLElements {
};
'svelte:head': { [name: string]: any };
'svelte:boundary': {
onerror?: (error: unknown, reset: () => void) => void;
failed?: import('svelte').Snippet<[error: unknown, reset: () => void]>;
pending?: import('svelte').Snippet;
onerror?: ((error: unknown, reset: () => void) => void) | null | undefined;
failed?: import('svelte').Snippet<[error: unknown, reset: () => void]> | null | undefined;
pending?: import('svelte').Snippet | null | undefined;
};
[name: string]: { [name: string]: any };

@ -2,7 +2,7 @@
"name": "svelte",
"description": "Cybernetically enhanced web apps",
"license": "MIT",
"version": "5.55.5",
"version": "5.55.8",
"type": "module",
"types": "./types/index.d.ts",
"engines": {
@ -176,7 +176,7 @@
"aria-query": "5.3.1",
"axobject-query": "^4.1.0",
"clsx": "^2.1.1",
"devalue": "^5.6.4",
"devalue": "^5.8.1",
"esm-env": "^1.2.1",
"esrap": "^2.2.4",
"is-reference": "^3.0.3",

@ -9,5 +9,9 @@ import { build_inline_component } from './shared/component.js';
* @param {ComponentContext} context
*/
export function Component(node, context) {
build_inline_component(node, /** @type {Expression} */ (context.visit(b.id(node.name))), context);
build_inline_component(
node,
/** @type {Expression} */ (context.visit(b.member_id(node.name))),
context
);
}

@ -12,7 +12,7 @@ import {
import * as b from '#compiler/builders';
import { sanitize_template_string } from '../../../../../utils/sanitize_template_string.js';
import { regex_whitespaces_strict } from '../../../../patterns.js';
import { has_await_expression } from '../../../../../utils/ast.js';
import { has_await_expression, save } from '../../../../../utils/ast.js';
import { ExpressionMetadata } from '../../../../nodes.js';
/** Opens an if/each block, so that we can remove nodes in the case of a mismatch */
@ -229,26 +229,35 @@ export function build_attribute_value(
? node.data.replace(regex_whitespaces_strict, ' ')
: node.data;
} else {
expressions.push(
b.call(
'$.stringify',
transform(
const evaluated = context.state.scope.evaluate(node.expression);
if (evaluated.is_known) {
quasi.value.cooked += (evaluated.value ?? '') + '';
} else {
const expression = transform(
/** @type {Expression} */ (context.visit(node.expression)),
node.metadata.expression
)
)
);
expressions.push(
evaluated.is_string && evaluated.is_defined
? expression
: b.call('$.stringify', expression)
);
quasi = b.quasi('', i + 1 === value.length);
quasis.push(quasi);
}
}
}
for (const quasi of quasis) {
quasi.value.raw = sanitize_template_string(/** @type {string} */ (quasi.value.cooked));
}
return b.template(quasis, expressions);
return expressions.length > 0
? b.template(quasis, expressions)
: b.literal(/** @type {string} */ (quasi.value.cooked));
}
/**
@ -360,7 +369,7 @@ export class PromiseOptimiser {
return b.const(
b.array_pattern(this.expressions.map((_, i) => b.id(`$$${i}`))),
b.await(b.call('Promise.all', promises))
save(b.call('Promise.all', promises))
);
}

@ -247,7 +247,7 @@ const css_visitors = {
},
Percentage(node, context) {
context.write(`${node.value}%`);
context.write(node.value);
},
PseudoClassSelector(node, context) {
@ -417,6 +417,7 @@ const svelte_visitors = (comments) => ({
const is_block_element =
child_node.type === 'RegularElement' ||
child_node.type === 'Component' ||
child_node.type === 'SvelteBody' ||
child_node.type === 'SvelteHead' ||
child_node.type === 'SvelteFragment' ||
child_node.type === 'SvelteBoundary' ||
@ -821,6 +822,10 @@ const svelte_visitors = (comments) => ({
context.write('</style>');
},
SvelteBody(node, context) {
base_element(node, context, comments);
},
SvelteBoundary(node, context) {
base_element(node, context, comments);
},

@ -32,7 +32,7 @@ export const ELEMENT_IS_NAMESPACED = 1;
export const ELEMENT_PRESERVE_ATTRIBUTE_CASE = 1 << 1;
export const ELEMENT_IS_INPUT = 1 << 2;
export const UNINITIALIZED = Symbol();
export const UNINITIALIZED = Symbol('uninitialized');
// Dev-time component properties
export const FILENAME = Symbol('filename');

@ -63,6 +63,11 @@ export const STATE_SYMBOL = Symbol('$state');
export const LEGACY_PROPS = Symbol('legacy props');
export const LOADING_ATTR_SYMBOL = Symbol('');
export const PROXY_PATH_SYMBOL = Symbol('proxy path');
export const ATTRIBUTES_CACHE = Symbol('attributes');
export const CLASS_CACHE = Symbol('class');
export const STYLE_CACHE = Symbol('style');
export const TEXT_CACHE = Symbol('text');
export const FORM_RESET_HANDLER = Symbol('form reset');
/** An anchor might change, via this symbol on the original anchor we can tell HMR about the updated anchor */
export const HMR_ANCHOR = Symbol('hmr anchor');

@ -1,5 +1,5 @@
/** @import { Blocker, TemplateNode, Value } from '#client' */
import { flatten, increment_pending } from '../../reactivity/async.js';
import { flatten } from '../../reactivity/async.js';
import { get } from '../../runtime.js';
import {
hydrate_next,
@ -42,8 +42,6 @@ export function async(node, blockers = [], expressions = [], fn) {
return;
}
const decrement_pending = increment_pending();
if (was_hydrating) {
var previous_hydrate_node = hydrate_node;
set_hydrate_node(end);
@ -64,8 +62,6 @@ export function async(node, blockers = [], expressions = [], fn) {
if (was_hydrating) {
set_hydrating(false);
}
decrement_pending();
}
});
}

@ -35,19 +35,18 @@ import { queue_micro_task } from '../task.js';
import * as e from '../../errors.js';
import * as w from '../../warnings.js';
import { DEV } from 'esm-env';
import { Batch, current_batch, previous_batch, schedule_effect } from '../../reactivity/batch.js';
import { Batch, current_batch } from '../../reactivity/batch.js';
import { internal_set, source } from '../../reactivity/sources.js';
import { tag } from '../../dev/tracing.js';
import { createSubscriber } from '../../../../reactivity/create-subscriber.js';
import { create_text } from '../operations.js';
import { defer_effect } from '../../reactivity/utils.js';
import { set_signal_status } from '../../reactivity/status.js';
/**
* @typedef {{
* onerror?: (error: unknown, reset: () => void) => void;
* failed?: (anchor: Node, error: () => unknown, reset: () => () => void) => void;
* pending?: (anchor: Node) => void;
* onerror?: ((error: unknown, reset: () => void) => void) | null;
* failed?: ((anchor: Node, error: () => unknown, reset: () => () => void) => void) | null;
* pending?: ((anchor: Node) => void) | null;
* }} BoundaryProps
*/

@ -1,8 +1,8 @@
/** @import { TemplateNode } from '#client' */
import { hydrate_node, hydrating, set_hydrate_node, set_hydrating } from '../hydration.js';
import { create_text, get_first_child, get_next_sibling } from '../operations.js';
import { block } from '../../reactivity/effects.js';
import { COMMENT_NODE, EFFECT_PRESERVED, HEAD_EFFECT } from '#client/constants';
import { block, branch } from '../../reactivity/effects.js';
import { COMMENT_NODE, HEAD_EFFECT } from '#client/constants';
/**
* @param {string} hash
@ -49,9 +49,10 @@ export function head(hash, render_fn) {
}
try {
// normally a branch is the child of a block and would have the EFFECT_PRESERVED flag,
// but since head blocks don't necessarily only have direct branch children we add it on the block itself
block(() => render_fn(anchor), HEAD_EFFECT | EFFECT_PRESERVED);
block(() => {
var e = branch(() => render_fn(anchor));
e.f |= HEAD_EFFECT;
});
} finally {
if (was_hydrating) {
set_hydrating(true);

@ -5,7 +5,12 @@ import { get_descriptors, get_prototype_of } from '../../../shared/utils.js';
import { create_event, delegate, delegated, event, event_symbol } from './events.js';
import { add_form_reset_listener, autofocus } from './misc.js';
import * as w from '../../warnings.js';
import { IS_XHTML, LOADING_ATTR_SYMBOL } from '#client/constants';
import {
ATTRIBUTES_CACHE,
FORM_RESET_HANDLER,
IS_XHTML,
LOADING_ATTR_SYMBOL
} from '#client/constants';
import { queue_micro_task } from '../task.js';
import { is_capture_event, can_delegate_event, normalize_attribute } from '../../../../utils.js';
import {
@ -69,8 +74,7 @@ export function remove_input_defaults(input) {
}
};
// @ts-expect-error
input.__on_r = remove_defaults;
/** @type {any} */ (input)[FORM_RESET_HANDLER] = remove_defaults;
queue_micro_task(remove_defaults);
add_form_reset_listener();
}
@ -561,8 +565,7 @@ export function attribute_effect(
*/
function get_attributes(element) {
return /** @type {Record<string | symbol, unknown>} **/ (
// @ts-expect-error
element.__attributes ??= {
/** @type {any} */ (element)[ATTRIBUTES_CACHE] ??= {
[IS_CUSTOM_ELEMENT]: element.nodeName.includes('-'),
[IS_HTML]: element.namespaceURI === NAMESPACE_HTML
}
@ -583,13 +586,19 @@ function get_setters(element) {
var proto = element; // In the case of custom elements there might be setters on the instance
var element_proto = Element.prototype;
// Stop at Element, from there on there's only unnecessary setters we're not interested in
// Stop at Element, from there on there's only unnecessary (and dangerous, like innerHTML) setters we're not interested in
// Do not use constructor.name here as that's unreliable in some browser environments
while (element_proto !== proto) {
descriptors = get_descriptors(proto);
for (var key in descriptors) {
if (descriptors[key].set) {
if (
descriptors[key].set &&
// better safe than sorry, we don't want spread attributes to mess with HTML content
key !== 'innerHTML' &&
key !== 'textContent' &&
key !== 'innerText'
) {
setters.push(key);
}
}

@ -5,6 +5,7 @@ import {
set_active_effect,
set_active_reaction
} from '../../../runtime.js';
import { FORM_RESET_HANDLER } from '../../../constants.js';
import { add_form_reset_listener } from '../misc.js';
/**
@ -58,18 +59,15 @@ export function without_reactive_context(fn) {
*/
export function listen_to_event_and_reset_event(element, event, handler, on_reset = handler) {
element.addEventListener(event, () => without_reactive_context(handler));
// @ts-expect-error
const prev = element.__on_r;
const prev = /** @type {any} */ (element)[FORM_RESET_HANDLER];
if (prev) {
// special case for checkbox that can have multiple binds (group & checked)
// @ts-expect-error
element.__on_r = () => {
/** @type {any} */ (element)[FORM_RESET_HANDLER] = () => {
prev();
on_reset(true);
};
} else {
// @ts-expect-error
element.__on_r = () => on_reset(true);
/** @type {any} */ (element)[FORM_RESET_HANDLER] = () => on_reset(true);
}
add_form_reset_listener();

@ -1,4 +1,5 @@
import { to_class } from '../../../shared/attributes.js';
import { CLASS_CACHE } from '../../constants.js';
import { hydrating } from '../hydration.js';
/**
@ -11,8 +12,7 @@ import { hydrating } from '../hydration.js';
* @returns {Record<string, boolean> | undefined}
*/
export function set_class(dom, is_html, value, hash, prev_classes, next_classes) {
// @ts-expect-error need to add __className to patched prototype
var prev = dom.__className;
var prev = /** @type {any} */ (dom)[CLASS_CACHE];
if (
hydrating ||
@ -35,8 +35,7 @@ export function set_class(dom, is_html, value, hash, prev_classes, next_classes)
}
}
// @ts-expect-error need to add __className to patched prototype
dom.__className = value;
/** @type {any} */ (dom)[CLASS_CACHE] = value;
} else if (next_classes && prev_classes !== next_classes) {
for (var key in next_classes) {
var is_present = !!next_classes[key];

@ -237,9 +237,9 @@ export function handle_event_propagation(event) {
});
// This started because of Chromium issue https://chromestatus.com/feature/5128696823545856,
// where removal or moving of of the DOM can cause sync `blur` events to fire, which can cause logic
// where removal or moving of the DOM can cause sync `blur` events to fire, which can cause logic
// to run inside the current `active_reaction`, which isn't what we want at all. However, on reflection,
// it's probably best that all event handled by Svelte have this behaviour, as we don't really want
// it's probably best that all events handled by Svelte have this behaviour, as we don't really want
// an event handler to run in the context of another reaction or effect.
var previous_reaction = active_reaction;
var previous_effect = active_effect;

@ -1,6 +1,7 @@
import { hydrating } from '../hydration.js';
import { clear_text_content, get_first_child } from '../operations.js';
import { queue_micro_task } from '../task.js';
import { FORM_RESET_HANDLER } from '../../constants.js';
/**
* @param {HTMLElement} dom
@ -45,8 +46,7 @@ export function add_form_reset_listener() {
Promise.resolve().then(() => {
if (!evt.defaultPrevented) {
for (const e of /**@type {HTMLFormElement} */ (evt.target).elements) {
// @ts-expect-error
e.__on_r?.();
/** @type {any} */ (e)[FORM_RESET_HANDLER]?.();
}
}
});

@ -1,4 +1,5 @@
import { to_style } from '../../../shared/attributes.js';
import { STYLE_CACHE } from '../../constants.js';
import { hydrating } from '../hydration.js';
/**
@ -28,8 +29,7 @@ function update_styles(dom, prev = {}, next, priority) {
* @param {Record<string, any> | [Record<string, any>, Record<string, any>]} [next_styles]
*/
export function set_style(dom, value, prev_styles, next_styles) {
// @ts-expect-error
var prev = dom.__style;
var prev = /** @type {any} */ (dom)[STYLE_CACHE];
if (hydrating || prev !== value) {
var next_style_attr = to_style(value, next_styles);
@ -42,8 +42,7 @@ export function set_style(dom, value, prev_styles, next_styles) {
}
}
// @ts-expect-error
dom.__style = value;
/** @type {any} */ (dom)[STYLE_CACHE] = value;
} else if (next_styles) {
if (Array.isArray(next_styles)) {
update_styles(dom, prev_styles?.[0], next_styles[0]);

@ -5,7 +5,14 @@ import { init_array_prototype_warnings } from '../dev/equality.js';
import { get_descriptor, is_extensible } from '../../shared/utils.js';
import { active_effect } from '../runtime.js';
import { async_mode_flag } from '../../flags/index.js';
import { TEXT_NODE, REACTION_RAN } from '#client/constants';
import {
ATTRIBUTES_CACHE,
CLASS_CACHE,
REACTION_RAN,
STYLE_CACHE,
TEXT_CACHE,
TEXT_NODE
} from '#client/constants';
import { eager_block_effects } from '../reactivity/batch.js';
import { NAMESPACE_HTML } from '../../../constants.js';
@ -48,21 +55,15 @@ export function init_operations() {
if (is_extensible(element_prototype)) {
// the following assignments improve perf of lookups on DOM nodes
// @ts-expect-error
element_prototype.__click = undefined;
// @ts-expect-error
element_prototype.__className = undefined;
// @ts-expect-error
element_prototype.__attributes = null;
// @ts-expect-error
element_prototype.__style = undefined;
/** @type {any} */ (element_prototype)[CLASS_CACHE] = undefined;
/** @type {any} */ (element_prototype)[ATTRIBUTES_CACHE] = null;
/** @type {any} */ (element_prototype)[STYLE_CACHE] = undefined;
// @ts-expect-error
element_prototype.__e = undefined;
}
if (is_extensible(text_prototype)) {
// @ts-expect-error
text_prototype.__t = undefined;
/** @type {any} */ (text_prototype)[TEXT_CACHE] = undefined;
}
if (DEV) {

@ -55,33 +55,38 @@ export function flatten(blockers, sync, async, fn) {
/** @param {Value[]} values */
function finish(values) {
if ((parent.f & DESTROYED) !== 0) {
return;
}
restore();
try {
fn(values);
} catch (error) {
if ((parent.f & DESTROYED) === 0) {
invoke_error_boundary(error, parent);
}
}
unset_context();
}
var decrement_pending = increment_pending();
// Fast path: blockers but no async expressions
if (async.length === 0) {
/** @type {Promise<any>} */ (blocker_promise).then(() => finish(sync.map(d)));
/** @type {Promise<any>} */ (blocker_promise)
.then(() => finish(sync.map(d)))
.finally(decrement_pending);
return;
}
var decrement_pending = increment_pending();
// Full path: has async expressions
function run() {
Promise.all(async.map((expression) => async_derived(expression)))
.then((result) => finish([...sync.map(d), ...result]))
.catch((error) => invoke_error_boundary(error, parent))
.finally(() => decrement_pending());
.finally(decrement_pending);
}
if (blocker_promise) {
@ -213,22 +218,35 @@ export async function* for_await_track_reactivity_loss(iterable) {
throw new TypeError('value is not async iterable');
}
/** Whether the completion of the iterator was "normal", meaning it wasn't ended via `break` or a similar method */
let normal_completion = false;
// eslint-disable-next-line no-useless-assignment
let invoke_return = true;
try {
while (true) {
const { done, value } = (await track_reactivity_loss(iterator.next()))();
if (done) {
normal_completion = true;
invoke_return = false;
break;
}
var prev = reactivity_loss_tracker;
try {
yield value;
} catch (e) {
set_reactivity_loss_tracker(prev);
// If the yield throws, we need to call `return` but not return its value, instead rethrow
if (iterator.return !== undefined) {
(await track_reactivity_loss(iterator.return()))();
}
throw e;
}
set_reactivity_loss_tracker(prev);
}
} catch (error) {
invoke_return = false;
throw error;
} finally {
// If the iterator had an abrupt completion and `return` is defined on the iterator, call it and return the value
if (!normal_completion && iterator.return !== undefined) {
// If the iterator had an abrupt completion (break) and `return` is defined on the iterator, call it and return the value
if (invoke_return && iterator.return !== undefined) {
// eslint-disable-next-line no-unsafe-finally
return /** @type {TReturn} */ ((await track_reactivity_loss(iterator.return()))().value);
}
@ -285,6 +303,7 @@ export function run(thunks) {
.then(() => {
restore();
try {
if (errored) {
throw errored.error;
}
@ -294,6 +313,11 @@ export function run(thunks) {
}
return fn();
} finally {
// We gotta unset context directly in case the function returns a promise, in which case
// unset_context in .finally() would be too late ...
unset_context();
}
})
.catch(handle_error);
@ -302,6 +326,7 @@ export function run(thunks) {
promise.finally(() => {
blocker.settled = true;
// ... but we also need it after such a promise has resolved in case it restores our context
unset_context();
});
}
@ -310,7 +335,7 @@ export function run(thunks) {
// wait one more tick, so that template effects are
// guaranteed to run before `$effect(...)`
.then(() => Promise.resolve())
.finally(() => decrement_pending());
.finally(decrement_pending);
return blockers;
}
@ -334,8 +359,8 @@ export function increment_pending() {
boundary.update_pending_count(1, batch);
batch.increment(blocking, effect);
return (skip = false) => {
return () => {
boundary.update_pending_count(-1, batch);
batch.decrement(blocking, effect, skip);
batch.decrement(blocking, effect);
};
}

@ -16,7 +16,8 @@ import {
EAGER_EFFECT,
ERROR_VALUE,
MANAGED_EFFECT,
REACTION_RAN
REACTION_RAN,
DESTROYING
} from '#client/constants';
import { async_mode_flag } from '../../flags/index.js';
import { deferred, define_property, includes } from '../../shared/utils.js';
@ -33,7 +34,7 @@ import { flush_tasks, queue_micro_task } from '../dom/task.js';
import { DEV } from 'esm-env';
import { invoke_error_boundary } from '../error-handling.js';
import { flush_eager_effects, old_values, set_eager_effects, source, update } from './sources.js';
import { eager_effect, unlink_effect } from './effects.js';
import { eager_effect, teardown, unlink_effect } from './effects.js';
import { defer_effect } from './utils.js';
import { UNINITIALIZED } from '../../../constants.js';
import { set_signal_status } from './status.js';
@ -41,8 +42,11 @@ import { legacy_is_updating_store } from './store.js';
import { invariant } from '../../shared/dev.js';
import { log_effect_tree } from '../dev/debug.js';
/** @type {Set<Batch>} */
const batches = new Set();
/** @type {Batch | null} */
let first_batch = null;
/** @type {Batch | null} */
let last_batch = null;
/** @type {Batch | null} */
export let current_batch = null;
@ -85,13 +89,29 @@ export let collected_effects = null;
export let legacy_updates = null;
var flush_count = 0;
var source_stacks = DEV ? new Set() : null;
/** @type {Set<Value>} */
var source_stacks = new Set();
let uid = 1;
export class Batch {
id = uid++;
/** True as soon as `#process` was called */
#started = false;
linked = true;
/** @type {Batch | null} */
#prev = null;
/** @type {Batch | null} */
#next = null;
/** @type {Map<Effect, ReturnType<typeof deferred<any>>>} */
async_deriveds = new Map();
/**
* The current values of any signals that are updated in this batch.
* Tuple format: [value, is_derived] (note: is_derived is false for deriveds, too, if they were overridden via assignment)
@ -107,6 +127,13 @@ export class Batch {
*/
previous = new Map();
/**
* Async effects which this batch doesn't take into account anymore when calculating blockers,
* as it has a value for it already.
* @type {Set<Effect>}
*/
unblocked = new Set();
/**
* When the batch is committed (and the DOM is updated), we need to remove old branches
* and append new ones by calling the functions added inside (if/each/key/etc) blocks
@ -127,10 +154,9 @@ export class Batch {
#fork_commit_callbacks = new Set();
/**
* Async effects that are currently in flight
* @type {Map<Effect, number>}
* The number of async effects that are currently in flight
*/
#pending = new Map();
#pending = 0;
/**
* Async effects that are currently in flight, _not_ inside a pending boundary
@ -188,18 +214,12 @@ export class Batch {
#decrement_queued = false;
/** @type {Set<Batch>} */
#blockers = new Set();
#is_deferred() {
return this.is_fork || this.#blocking_pending.size > 0;
}
if (this.is_fork) return true;
#is_blocked() {
for (const batch of this.#blockers) {
for (const effect of batch.#blocking_pending.keys()) {
var skipped = false;
for (const effect of this.#blocking_pending.keys()) {
var e = effect;
var skipped = false;
while (e.parent !== null) {
if (this.#skipped_branches.has(e)) {
@ -214,7 +234,6 @@ export class Batch {
return true;
}
}
}
return false;
}
@ -255,11 +274,21 @@ export class Batch {
}
#process() {
this.#started = true;
if (flush_count++ > 1000) {
batches.delete(this);
this.#unlink();
infinite_loop_guard();
}
if (DEV) {
// track all the values that were updated during this flush,
// so that they can be reset afterwards
for (const value of this.current.keys()) {
source_stacks.add(value);
}
}
// we only reschedule previously-deferred effects if we expect
// to be able to run them after processing the batch
if (!this.#is_deferred()) {
@ -314,16 +343,27 @@ export class Batch {
collected_effects = null;
legacy_updates = null;
if (this.#is_deferred() || this.#is_blocked()) {
// if the batch has outstanding pending work, stash effects and bail
if (this.#is_deferred()) {
this.#defer_effects(render_effects);
this.#defer_effects(effects);
for (const [e, t] of this.#skipped_branches) {
reset_branch(e, t);
}
} else {
if (this.#pending.size === 0) {
batches.delete(this);
if (updates.length > 0) {
/** @type {Batch} */ (/** @type {unknown} */ (current_batch)).#process();
}
return;
}
const earlier_batch = this.#find_earlier_batch();
if (earlier_batch) {
earlier_batch.#merge(this);
return;
}
// clear effects. Those that are still needed will be rescheduled through unskipping the skipped branches.
@ -340,35 +380,37 @@ export class Batch {
previous_batch = null;
this.#deferred?.resolve();
var next_batch = /** @type {Batch | null} */ (/** @type {unknown} */ (current_batch));
if (this.linked && this.#pending === 0) {
this.#unlink();
}
// Order matters here - we need to commit and THEN continue flushing new batches, not the other way around,
// else we could start flushing a new batch and then, if it has pending work, rebase it right afterwards, which is wrong.
// In sync mode flushSync can cause #commit to wrongfully think that there needs to be a rebase, so we only do it in async mode
// TODO fix the underlying cause, otherwise this will likely regress when non-async mode is removed
if (async_mode_flag && !batches.has(this)) {
if (async_mode_flag && !this.linked) {
this.#commit();
// Rebases can activate other batches or null it out, therefore restore the new one here
current_batch = next_batch;
}
var next_batch = /** @type {Batch | null} */ (/** @type {unknown} */ (current_batch));
// Edge case: During traversal new branches might create effects that run immediately and set state,
// causing an effect and therefore a root to be scheduled again. We need to traverse the current batch
// once more in that case - most of the time this will just clean up dirty branches.
if (this.#roots.length > 0) {
const batch = (next_batch ??= this);
batch.#roots.push(...this.#roots.filter((r) => !batch.#roots.includes(r)));
if (next_batch === null) {
next_batch = this;
this.#link();
}
if (next_batch !== null) {
batches.add(next_batch);
if (DEV) {
for (const source of this.current.keys()) {
/** @type {Set<Source>} */ (source_stacks).add(source);
}
const batch = next_batch;
batch.#roots.push(...this.#roots.filter((r) => !batch.#roots.includes(r)));
}
if (next_batch !== null) {
next_batch.#process();
}
}
@ -425,6 +467,82 @@ export class Batch {
}
}
#find_earlier_batch() {
var batch = this.#prev;
while (batch !== null) {
if (!batch.is_fork) {
// if the batches are connected, break
for (const [value, [, is_derived]] of this.current) {
if (batch.current.has(value) && !is_derived) {
return batch;
}
}
}
batch = batch.#prev;
}
return null;
}
/**
* @param {Batch} batch
*/
#merge(batch) {
for (const [source, value] of batch.current) {
if (!this.previous.has(source) && batch.previous.has(source)) {
this.previous.set(source, batch.previous.get(source));
}
this.current.set(source, value);
}
for (const [effect, deferred] of batch.async_deriveds) {
const d = this.async_deriveds.get(effect);
if (d) deferred.promise.then(d.resolve);
}
/**
* mark all effects that depend on `batch.current`, except the
* async effects that we just resolved (TODO unless they depend
* on values in this batch that are NOT in the later batch?).
* Through this we also will populate the correct #skipped_branches,
* oncommit callbacks etc, so we don't need to merge them separately.
* @param {Value} value
*/
const mark = (value) => {
var reactions = value.reactions;
if (reactions === null) return;
for (const reaction of reactions) {
var flags = reaction.f;
if ((flags & DERIVED) !== 0) {
mark(/** @type {Derived} */ (reaction));
} else {
var effect = /** @type {Effect} */ (reaction);
if (flags & (ASYNC | BLOCK_EFFECT) && !this.async_deriveds.has(effect)) {
this.#maybe_dirty_effects.delete(effect);
set_signal_status(effect, DIRTY);
this.schedule(effect);
}
}
}
};
for (const source of this.current.keys()) {
mark(source);
}
this.oncommit(() => batch.discard());
batch.#unlink();
current_batch = this;
this.#process();
}
/**
* @param {Effect[]} effects
*/
@ -467,9 +585,11 @@ export class Batch {
}
flush() {
var source_stacks = DEV ? new Set() : null;
try {
if (DEV) {
source_stacks.clear();
}
is_processing = true;
current_batch = this;
@ -487,7 +607,7 @@ export class Batch {
old_values.clear();
if (DEV) {
for (const source of /** @type {Set<Source>} */ (source_stacks)) {
for (const source of source_stacks) {
source.updated = null;
}
}
@ -499,7 +619,7 @@ export class Batch {
this.#discard_callbacks.clear();
this.#fork_commit_callbacks.clear();
batches.delete(this);
this.#unlink();
}
/**
@ -510,11 +630,13 @@ export class Batch {
}
#commit() {
this.#unlink();
// If there are other pending batches, they now need to be 'rebased' —
// in other words, we re-run block/async effects with the newly
// committed state, unless the batch in question has a more
// recent value for a given source
for (const batch of batches) {
for (let batch = first_batch; batch !== null; batch = batch.#next) {
var is_earlier = batch.id < this.id;
/** @type {Source[]} */
@ -537,6 +659,17 @@ export class Batch {
sources.push(source);
}
if (is_earlier) {
// TODO do we need to restart these in some cases, instead of
// immediately resolving them? Likely not because of how this.apply() works.
for (const [effect, deferred] of this.async_deriveds) {
const d = batch.async_deriveds.get(effect);
if (d) deferred.promise.then(d.resolve);
}
}
if (!batch.#started) continue;
// Re-run async/block effects that depend on distinct values changed in both batches
var others = [...batch.current.keys()].filter((s) => !this.current.has(s));
@ -612,17 +745,6 @@ export class Batch {
batch.deactivate();
}
}
for (const batch of batches) {
if (batch.#blockers.has(this)) {
batch.#blockers.delete(this);
if (batch.#blockers.size === 0 && !batch.#is_deferred()) {
batch.activate();
batch.#process();
}
}
}
}
/**
@ -630,8 +752,7 @@ export class Batch {
* @param {Effect} effect
*/
increment(blocking, effect) {
let pending_count = this.#pending.get(effect) ?? 0;
this.#pending.set(effect, pending_count + 1);
this.#pending += 1;
if (blocking) {
let blocking_pending_count = this.#blocking_pending.get(effect) ?? 0;
@ -642,16 +763,9 @@ export class Batch {
/**
* @param {boolean} blocking
* @param {Effect} effect
* @param {boolean} skip - whether to skip updates (because this is triggered by a stale reaction)
*/
decrement(blocking, effect, skip) {
let pending_count = this.#pending.get(effect) ?? 0;
if (pending_count === 1) {
this.#pending.delete(effect);
} else {
this.#pending.set(effect, pending_count - 1);
}
decrement(blocking, effect) {
this.#pending -= 1;
if (blocking) {
let blocking_pending_count = this.#blocking_pending.get(effect) ?? 0;
@ -663,12 +777,15 @@ export class Batch {
}
}
if (this.#decrement_queued || skip) return;
if (this.#decrement_queued) return;
this.#decrement_queued = true;
queue_micro_task(() => {
this.#decrement_queued = false;
if (this.linked) {
this.flush();
}
});
}
@ -716,20 +833,14 @@ export class Batch {
static ensure() {
if (current_batch === null) {
const batch = (current_batch = new Batch());
batch.#link();
if (!is_processing) {
batches.add(current_batch);
if (!is_flushing_sync) {
if (!is_processing && !is_flushing_sync) {
queue_micro_task(() => {
if (!batches.has(batch) || batch.#pending.size > 0) {
// a flushSync happened in the meantime
return;
}
if (!batch.#started) {
batch.flush();
});
}
});
}
}
@ -737,7 +848,7 @@ export class Batch {
}
apply() {
if (!async_mode_flag || (!this.is_fork && batches.size === 1)) {
if (!async_mode_flag || (!this.is_fork && this.#prev === null && this.#next === null)) {
batch_values = null;
return;
}
@ -749,28 +860,33 @@ export class Batch {
batch_values.set(source, value);
}
// ...and undo changes belonging to other batches unless they block this one
for (const batch of batches) {
// ...and undo changes belonging to other batches unless they intersect
for (let batch = first_batch; batch !== null; batch = batch.#next) {
if (batch === this || batch.is_fork) continue;
// A batch is blocked on an earlier batch if it overlaps with the earlier batch's changes but is not a superset
// If two batches intersect, the latter batch will be merged into the earlier batch,
// and we should treat them as a single set of changes
var intersects = false;
var differs = false;
if (batch.id < this.id) {
for (const [source, [, is_derived]] of batch.current) {
// Derived values don't partake in the blocking mechanism, because a derived could
// Derived values don't partake in the intersection mechanism, because a derived could
// be triggered in one batch already but not the other one yet, causing a false-positive
if (is_derived) continue;
intersects ||= this.current.has(source);
differs ||= !this.current.has(source);
if (this.current.has(source)) {
intersects = true;
break;
}
}
}
if (intersects && differs) {
this.#blockers.add(batch);
} else {
// Since the latter batch merges into the earlier (if it resolves before the earlier one),
// we treat the earlier values as "already applied". This way we don't need to rerun async
// effects of the earlier batch in case they are merged.
// As a result you can think of batch_values as having the latest values of all intersecting
// batches up until this batch.
if (!intersects) {
for (const [source, previous] of batch.previous) {
if (!batch_values.has(source)) {
batch_values.set(source, previous);
@ -836,6 +952,36 @@ export class Batch {
this.#roots.push(e);
}
#link() {
if (last_batch === null) {
first_batch = last_batch = this;
} else {
last_batch.#next = this;
this.#prev = last_batch;
}
last_batch = this;
}
#unlink() {
var prev = this.#prev;
var next = this.#next;
if (prev === null) {
first_batch = next;
} else {
prev.#next = next;
}
if (next === null) {
last_batch = prev;
} else {
next.#prev = prev;
}
this.linked = false;
}
}
// TODO Svelte@6 think about removing the callback argument.
@ -1089,6 +1235,9 @@ function eager_flush() {
});
}
/** @type {Map<Reaction, Source<number>>} */
var version_map = new Map();
/**
* Implementation of `$state.eager(fn())`
* @template T
@ -1096,10 +1245,22 @@ function eager_flush() {
* @returns {T}
*/
export function eager(fn) {
var version = source(0);
var initial = true;
var value = /** @type {T} */ (undefined);
if (active_reaction === null) {
return fn();
}
let parent = active_reaction;
let version = version_map.get(parent) ?? source(0);
version_map.set(parent, version);
teardown(() => {
if (parent.f & DESTROYING) version_map.delete(parent);
});
get(version);
eager_effect(() => {
@ -1219,7 +1380,7 @@ export function fork(fn) {
return;
}
if (!batches.has(batch)) {
if (!batch.linked) {
e.fork_discarded();
}
@ -1265,7 +1426,7 @@ export function fork(fn) {
source.wv = increment_write_version();
}
if (!committed && batches.has(batch)) {
if (!committed && batch.linked) {
batch.discard();
}
}
@ -1276,5 +1437,5 @@ export function fork(fn) {
* Forcibly remove all current batches, to prevent cross-talk between tests
*/
export function clear() {
batches.clear();
first_batch = last_batch = null;
}

@ -100,6 +100,8 @@ export function derived(fn) {
return signal;
}
export const OBSOLETE = Symbol('obsolete');
/**
* @template V
* @param {() => V | Promise<V>} fn
@ -118,13 +120,13 @@ export function async_derived(fn, label, location) {
var promise = /** @type {Promise<V>} */ (/** @type {unknown} */ (undefined));
var signal = source(/** @type {V} */ (UNINITIALIZED));
if (DEV) signal.label = label;
if (DEV) signal.label = label ?? fn.toString();
// only suspend in async deriveds created on initialisation
var should_suspend = !active_reaction;
/** @type {Map<Batch, ReturnType<typeof deferred<V>>>} */
var deferreds = new Map();
/** @type {Set<ReturnType<typeof deferred<V>>>} */
var deferreds = new Set();
async_effect(() => {
var effect = /** @type {Effect} */ (active_effect);
@ -141,7 +143,13 @@ export function async_derived(fn, label, location) {
// If this code is changed at some point, make sure to still access the then property
// of fn() to read any signals it might access, so that we track them as dependencies.
// We call `unset_context` to undo any `save` calls that happen inside `fn()`
Promise.resolve(fn()).then(d.resolve, d.reject).finally(unset_context);
Promise.resolve(fn())
.then(d.resolve, (e) => {
// if the promise was rejected by the user, via `getAbortSignal`, then
// wait for a subsequent resolution instead of flushing the batch
if (e !== STALE_REACTION) d.reject(e);
})
.finally(unset_context);
} catch (error) {
d.reject(error);
unset_context();
@ -180,18 +188,17 @@ export function async_derived(fn, label, location) {
}
if (/** @type {Boundary} */ (parent.b).is_rendered()) {
deferreds.get(batch)?.reject(STALE_REACTION);
deferreds.delete(batch); // delete to ensure correct order in Map iteration below
batch.async_deriveds.get(effect)?.reject(OBSOLETE);
} else {
// While the boundary is still showing pending, a new run supersedes all older in-flight runs
// for this async expression. Cancel eagerly so resolution cannot commit stale values.
for (const d of deferreds.values()) {
d.reject(STALE_REACTION);
d.reject(OBSOLETE);
}
deferreds.clear();
}
deferreds.set(batch, d);
deferreds.add(d);
batch.async_deriveds.set(effect, d);
}
/**
@ -203,16 +210,10 @@ export function async_derived(fn, label, location) {
reactivity_loss_tracker = null;
}
if (decrement_pending) {
// don't trigger an update if we're only here because
// the promise was superseded before it could resolve
var skip = error === STALE_REACTION;
decrement_pending(skip);
}
decrement_pending?.();
deferreds.delete(d);
if (error === STALE_REACTION || (effect.f & DESTROYED) !== 0) {
return;
}
if (error === OBSOLETE) return;
batch.activate();
@ -228,18 +229,11 @@ export function async_derived(fn, label, location) {
internal_set(signal, value);
// All prior async derived runs are now stale
for (const [b, d] of deferreds) {
deferreds.delete(b);
if (b === batch) break;
d.resolve(value);
}
if (DEV && location !== undefined) {
recent_async_deriveds.add(signal);
setTimeout(() => {
if (recent_async_deriveds.has(signal)) {
if (recent_async_deriveds.has(signal) && (effect.f & DESTROYED) === 0) {
w.await_waterfall(/** @type {string} */ (signal.label), location);
recent_async_deriveds.delete(signal);
}
@ -254,8 +248,8 @@ export function async_derived(fn, label, location) {
});
teardown(() => {
for (const d of deferreds.values()) {
d.reject(STALE_REACTION);
for (const d of deferreds) {
d.reject(OBSOLETE);
}
});
@ -344,7 +338,12 @@ export function execute_derived(derived) {
var prev_active_effect = active_effect;
var parent = derived.parent;
if (!is_destroying_effect && parent !== null && (parent.f & (DESTROYED | INERT)) !== 0) {
if (
!is_destroying_effect &&
parent !== null &&
derived.v !== UNINITIALIZED && // if it was never evaluated before, it's guaranteed to fail downstream, so we try to execute instead
(parent.f & (DESTROYED | INERT)) !== 0
) {
w.derived_inert();
return derived.v;
@ -453,8 +452,8 @@ export function freeze_derived_effects(derived) {
// make it a noop so it doesn't get called again if the derived
// is unfrozen. we don't set it to `null`, because the existence
// of a teardown function is what determines whether the
// effect runs again during unfreezing
e.teardown = noop;
// effect runs again during unfreezing (but not for teardown-only effects)
if (e.fn !== null) e.teardown = noop;
e.ac = null;
remove_reactions(e, 0);
@ -472,7 +471,7 @@ export function unfreeze_derived_effects(derived) {
for (const e of derived.effects) {
// if the effect was previously frozen — indicated by the presence
// of a teardown function — unfreeze it
if (e.teardown) {
if (e.teardown && e.fn !== null) {
update_effect(e);
}
}

@ -43,7 +43,7 @@ import { define_property } from '../../shared/utils.js';
import { get_next_sibling } from '../dom/operations.js';
import { component_context, dev_current_component_function, dev_stack } from '../context.js';
import { Batch, collected_effects, current_batch } from './batch.js';
import { flatten, increment_pending } from './async.js';
import { flatten } from './async.js';
import { without_reactive_context } from '../dom/elements/bindings/shared.js';
import { set_signal_status } from './status.js';
@ -396,16 +396,8 @@ export function template_effect(fn, sync = [], async = [], blockers = []) {
* @param {Blocker[]} blockers
*/
export function deferred_template_effect(fn, sync = [], async = [], blockers = []) {
if (async.length > 0 || blockers.length > 0) {
var decrement_pending = increment_pending();
}
flatten(blockers, sync, async, (values) => {
create_effect(EFFECT, () => fn(...values.map(get)));
if (decrement_pending) {
decrement_pending();
}
});
}

@ -21,7 +21,7 @@ export let legacy_is_updating_store = false;
*/
let is_store_binding = false;
let IS_UNMOUNTED = Symbol();
let IS_UNMOUNTED = Symbol('unmounted');
/**
* Gets the current value of a store. If the store isn't subscribed to yet, it will create a proxy

@ -23,7 +23,7 @@ import * as w from './warnings.js';
import * as e from './errors.js';
import { assign_nodes } from './dom/template.js';
import { is_passive_event } from '../../utils.js';
import { COMMENT_NODE, STATE_SYMBOL } from './constants.js';
import { COMMENT_NODE, STATE_SYMBOL, TEXT_CACHE } from './constants.js';
import { boundary } from './dom/blocks/boundary.js';
/**
@ -46,10 +46,9 @@ export function set_should_intro(value) {
export function set_text(text, value) {
// For objects, we apply string coercion (which might make things like $state array references in the template reactive) before diffing
var str = value == null ? '' : typeof value === 'object' ? `${value}` : value;
// @ts-expect-error
if (str !== (text.__t ??= text.nodeValue)) {
// @ts-expect-error
text.__t = str;
// prettier-ignore
if (str !== (/** @type {any} */ (text)[TEXT_CACHE] ??= text.nodeValue)) {
/** @type {any} */ (text)[TEXT_CACHE] = str;
text.nodeValue = `${str}`;
}
}

@ -3,7 +3,6 @@ import { async_mode_flag } from '../flags/index.js';
import { get_render_context } from './render-context.js';
import * as e from './errors.js';
import * as devalue from 'devalue';
import { get_stack } from '../shared/dev.js';
import { DEV } from 'esm-env';
import { get_user_code_location } from './dev.js';
@ -65,7 +64,13 @@ function encode(key, value, unresolved) {
const placeholder = `"${uid++}"`;
const p = value
.then((v) => {
entry.serialized = entry.serialized.replace(placeholder, `r(${uneval(v)})`);
entry.serialized = entry.serialized.replace(
placeholder,
// use the function form here to prevent any string replacement characters from being interpreted
// in `v`, as it's potentially user-controlled and therefore potentially malicious.
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace#specifying_a_string_as_the_replacement
() => `r(${uneval(v)})`
);
})
.catch((devalue_error) =>
e.hydratable_serialization_failed(

@ -0,0 +1,33 @@
import { afterAll, beforeAll, expect, test } from 'vitest';
import { Renderer } from './renderer.js';
import type { Component } from 'svelte';
import { disable_async_mode_flag, enable_async_mode_flag } from '../flags/index.js';
import { hydratable } from './hydratable.js';
beforeAll(() => {
enable_async_mode_flag();
});
afterAll(() => {
disable_async_mode_flag();
});
test('treats replacement tokens in hydratable promise values as literals', async () => {
const component = (renderer: Renderer) => {
hydratable('key', () => Promise.resolve(`$'`));
renderer.child(async () => {
await Promise.resolve();
});
renderer.push('ok');
};
const { head } = await Renderer.render(component as unknown as Component);
const script_match = head.match(/<script(?:\s[^>]*)?>([\s\S]*)<\/script>/);
expect(script_match, 'expected hydratable script in head output').toBeTruthy();
const script_content = script_match![1];
expect(script_content).toContain('const h = (window.__svelte ??= {}).h ??= new Map();');
expect(script_content).toContain('r("$\'")');
expect(script_content).toMatch(/\[\s*"key"\s*,\s*r\("\$'"\)\s*\]/);
});

@ -151,7 +151,7 @@ export function attributes(attrs, css_hash, classes, styles, flags = 0) {
// omit functions, internal svelte properties and invalid attribute names
if (typeof attrs[name] === 'function') continue;
if (name[0] === '$' && name[1] === '$') continue; // faster than name.startsWith('$$')
if (INVALID_ATTR_NAME_CHAR_REGEX.test(name)) continue;
if (name === '' || INVALID_ATTR_NAME_CHAR_REGEX.test(name)) continue;
var value = attrs[name];
var lower = name.toLowerCase();

@ -715,7 +715,12 @@ export class Renderer {
const { context, failed, transformError } = item.#boundary;
set_ssr_context(context);
let transformed = await transformError(error);
let promise = transformError(error);
set_ssr_context(null);
let transformed = await promise;
set_ssr_context(context);
// Render the failed snippet instead of the partial children content
const failed_renderer = new Renderer(item.global, item);

@ -4,7 +4,7 @@ import { tag } from '../internal/client/dev/tracing.js';
import { get } from '../internal/client/runtime.js';
import { get_current_url } from './url.js';
export const REPLACE = Symbol();
export const REPLACE = Symbol('replace');
/**
* A reactive version of the built-in [`URLSearchParams`](https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams) object.

@ -491,7 +491,7 @@ export function is_raw_text_element(name) {
// Rejects strings containing whitespace, quotes, angle brackets, slashes, equals,
// or other characters that could break out of a tag-name token and enable markup injection.
export const REGEX_VALID_TAG_NAME =
/^[a-zA-Z][a-zA-Z0-9]*(-[a-zA-Z0-9.\-_\u00B7\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u037D\u037F-\u1FFF\u200C-\u200D\u203F-\u2040\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\u{10000}-\u{EFFFF}]+)*$/u;
/^[a-zA-Z][a-zA-Z0-9]*(-[a-zA-Z0-9.\-_\u00B7\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u037D\u037F-\u1FFF\u200C-\u200D\u203F-\u2040\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\u{10000}-\u{EFFFF}]*)?$/u;
/**
* Prevent devtools trying to make `location` a clickable link by inserting a zero-width space

@ -0,0 +1,50 @@
import { expect, test } from 'vitest';
import { REGEX_VALID_TAG_NAME } from './utils';
test('REGEX_VALID_TAG_NAME accepts common HTML tag names', () => {
const common_html_tag_names = ['div', 'span', 'button', 'input', 'svg', 'math', 'a'];
for (const tag_name of common_html_tag_names) {
expect(REGEX_VALID_TAG_NAME.test(tag_name)).toBe(true);
}
});
test('REGEX_VALID_TAG_NAME accepts basic custom element names', () => {
const valid_custom_tag_names = ['my-element', 'x-foo', 'todo-item', 'my-element2'];
for (const tag_name of valid_custom_tag_names) {
expect(REGEX_VALID_TAG_NAME.test(tag_name)).toBe(true);
}
});
test('REGEX_VALID_TAG_NAME accepts spec-allowed custom element characters', () => {
const valid_custom_tag_names = [
'x-foo.bar',
'x-foo_bar',
'x-foo\u00B7bar',
'x-foo\u00FCbar',
'x-foo\u{1F600}bar',
'x-'
];
for (const tag_name of valid_custom_tag_names) {
expect(REGEX_VALID_TAG_NAME.test(tag_name)).toBe(true);
}
});
test('REGEX_VALID_TAG_NAME rejects invalid tag names', () => {
const invalid_tag_names = ['', '1', 'x\u00FC', '-x-foo', '1foo', 'x-foo bar', 'x-foo/', 'x-foo>'];
for (const tag_name of invalid_tag_names) {
expect(REGEX_VALID_TAG_NAME.test(tag_name)).toBe(false);
}
});
test('REGEX_VALID_TAG_NAME no ReDoS', () => {
const before = performance.now();
REGEX_VALID_TAG_NAME.test('a-----------------------------------!');
const after = performance.now();
if (after - before > 10) {
throw new Error(`REGEX_VALID_TAG_NAME is vulnerable to ReDoS`);
}
});

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

@ -0,0 +1,7 @@
<style>
@keyframes foo {
0% { left: 0px; }
50% { left: 50px; }
100% { left: 100px; }
}
</style>

@ -0,0 +1,13 @@
<style>
@keyframes foo {
0% {
left: 0px;
}
50% {
left: 50px;
}
100% {
left: 100px;
}
}
</style>

@ -19,7 +19,7 @@
from {
opacity: 0;
}
50%% {
50% {
opacity: 0.5;
}
to {

@ -0,0 +1 @@
<svelte:body onmousemove={handleMousemove} />

@ -0,0 +1 @@
<svelte:body onmousemove={handleMousemove}></svelte:body>

@ -0,0 +1,23 @@
import { assert_ok, test } from '../../assert';
export default test({
async test({ assert, target, waitUntil, window }) {
const form = target.querySelector('form');
const button = target.querySelector('button');
const [i1, i2, i3] = target.querySelectorAll('input');
assert_ok(form);
assert_ok(button);
assert.equal(form.id, 'initial-form');
assert.equal(form.className, 'first');
assert.equal(window.getComputedStyle(form).backgroundColor, 'rgb(255, 0, 0)');
button.click();
await waitUntil(() => form.id === 'updated-form');
assert.equal(form.id, 'updated-form');
assert.equal(form.className, 'second');
assert.equal(i3.id, '', 'input clobbered form');
assert.equal(window.getComputedStyle(form).backgroundColor, 'rgb(0, 0, 255)');
}
});

@ -0,0 +1,20 @@
<script>
let form_attributes = $state({ id: 'initial-form' });
let class_name = $state('first');
let background_color = $state('rgb(255, 0, 0)');
function update() {
form_attributes = { id: 'updated-form' };
class_name = 'second';
background_color = 'rgb(0, 0, 255)';
}
</script>
<button onclick={update}>update</button>
<form {...form_attributes} class={class_name} style:background-color={background_color}>
<!-- the once non-symbol-ified hidden properties we used -->
<input {...{ name: '__className' }} value="x" />
<input {...{ name: '__style' }} value="y" />
<input {...{ name: '__attributes' }} value="z" />
</form>

@ -60,6 +60,8 @@ export interface RuntimeTest<Props extends Record<string, any> = Record<string,
id_prefix?: string;
before_test?: () => void;
after_test?: () => void;
/** If true, flushSync() will not be called before invoking test() */
skip_initial_flushSync?: boolean;
test?: (args: {
variant: 'dom' | 'hydrate';
assert: Assert;
@ -505,7 +507,7 @@ async function run_test_variant(
try {
if (config.test) {
flushSync();
if (!config.skip_initial_flushSync) flushSync();
if (variant === 'hydrate' && cwd.includes('async-')) {
// wait for pending boundaries to render
@ -543,7 +545,7 @@ async function run_test_variant(
}
} finally {
if (runes) {
unmount(instance);
await unmount(instance);
} else {
instance.$destroy();
}

@ -0,0 +1,30 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target }) {
await tick();
const [increment, shift, middle] = target.querySelectorAll('button');
const [div] = target.querySelectorAll('div');
increment.click();
await tick();
increment.click();
await tick();
increment.click();
await tick();
middle.click(); // resolve the second increment which will make the if block go away and the first batch discarded
await tick();
assert.htmlEqual(div.innerHTML, '2 2');
shift.click();
await tick();
shift.click();
await tick();
shift.click();
await tick();
shift.click();
await tick();
assert.htmlEqual(div.innerHTML, '3 3');
}
});

@ -0,0 +1,21 @@
<script>
let a = $state(0);
const deferred = [];
function delay(value) {
if (!value) return value;
return new Promise((resolve) => deferred.push(() => resolve(value)));
}
</script>
<div>
{a} {await delay(a)}
{#if a < 2}
{await delay(a)}
{/if}
</div>
<button onclick={() => {a++;}}>a++</button>
<button onclick={() => deferred.shift()?.()}>shift</button>
<button onclick={() => deferred[2]()}>middle</button>

@ -0,0 +1,19 @@
import { tick } from 'svelte';
import { test } from '../../test';
// Tests that batch.#commit() does not null out a potentially new current_batch
export default test({
skip_initial_flushSync: true, // test that the initial batch is flushed without an explicit flushSync() call
async test({ assert, target }) {
await tick();
const [button] = target.querySelectorAll('button');
const [updates] = target.querySelectorAll('p');
assert.htmlEqual(updates.innerHTML, 'false');
button.click();
await tick();
assert.htmlEqual(updates.innerHTML, 'true');
}
});

@ -0,0 +1,30 @@
<script>
let count = $state(-1);
let payload = $state(false);
let updated = $state(false);
$effect(() => {
if (payload) {
updated = true;
}
});
function update() {
count = 0;
queueMicrotask(() => {
payload = true;
});
}
</script>
<button onclick={update}>update</button>
<p>{updated}</p>
<svelte:boundary>
{await new Promise(() => {})}
{#snippet pending()}
<p>pending</p>
{/snippet}
</svelte:boundary>

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

@ -0,0 +1,20 @@
<script>
let count = $state(0);
const queued = [];
async function delay(v) {
if (!v) return v;
return new Promise(r => queued.push(() => r(v)));
}
</script>
<button onclick={() => count++}>increment</button>
<button onclick={() => queued.shift()?.()}>resolve</button>
{await delay(count)}
{#if $state.eager(count) !== count}
<p>loading...</p>
{:else}
<p>{count}</p>
{/if}

@ -0,0 +1,32 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target, logs }) {
await tick();
const [increment, resolve] = target.querySelectorAll('button');
logs.length = 0;
increment.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`<button>increment</button> <button>resolve</button>
<ul><li>0 / 0</li><li>0 / loading...</li><li>0 / 0</li></ul>`
);
resolve.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`<button>increment</button> <button>resolve</button>
<ul><li>0 / 0</li><li>1 / 1</li><li>0 / 0</li></ul>`
);
assert.equal(
logs.some((l) => l.toString().includes('0 ') || l.toString().includes('2')),
false,
'only the second $state.eager should have been evaluated'
);
}
});

@ -0,0 +1,24 @@
<script>
let counts = $state([0, 0, 0]);
const queued = [];
async function delay(v) {
if (!v) return v;
return new Promise(r => queued.push(() => r(v)));
}
</script>
<button onclick={() => counts[1]++}>increment</button>
<button onclick={() => queued.shift()?.()}>resolve</button>
<ul>
{#each counts as count, i}
<li>
{await delay(count)} /
{#if console.log(i) || $state.eager(count) !== count}
loading...
{:else}
{count}
{/if}
</li>
{/each}
</ul>

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

@ -0,0 +1,25 @@
<script lang="ts">
import { flushSync } from 'svelte';
let count = $state(0);
const queue: Array<() => void> = [];
$effect(() => {
if (count === 1) {
count = 2;
flushSync();
}
})
function push(v: number) {
if (v === 0) return v;
return new Promise(r => queue.push(() => r(v)));
}
</script>
<button onclick={() => count += 1}>
clicks: {count}
</button>
<button onclick={() => queue.shift()?.()}>shift</button>
{await push(count)}

@ -34,18 +34,6 @@ export default test({
shift?.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<p>true</p>
<button>toggle</button>
<button>shift</button>
`
);
shift?.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`

@ -0,0 +1,20 @@
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();
increment.click();
await tick();
pop.click();
await tick();
assert.htmlEqual(target.innerHTML, `<button>increment</button> <button>pop</button> 2 2 1`);
pop.click();
await tick();
assert.htmlEqual(target.innerHTML, `<button>increment</button> <button>pop</button> 2 2 1`);
}
});

@ -0,0 +1,26 @@
<script>
let count = $state(0);
let other = $state(0);
const queue = [];
function push(v) {
return new Promise((r,e) => queue.push(() => v === 1 ? e(v) : r(v)));
}
</script>
<button onclick={() => {
if (count === 0) {
other++;
count++;
} else {
count++
}
}}>increment</button>
<button onclick={() => queue.pop()?.()}>pop</button>
{#if count > 0}
<svelte:boundary>
{await push(count)} {count} {other}
{#snippet failed()}boom{/snippet}
</svelte:boundary>
{/if}

@ -0,0 +1,6 @@
<script>
let { count } = $props();
let double = $derived(count * 2);
$effect.pre(() => console.log(count, double));
</script>

@ -0,0 +1,27 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target, logs, warnings }) {
const [increment, resolve] = target.querySelectorAll('button');
increment.click();
await tick();
assert.deepEqual(logs, []);
resolve.click();
await tick();
assert.deepEqual(logs, []);
resolve.click();
await tick();
assert.deepEqual(logs, []);
resolve.click();
await tick();
assert.deepEqual(logs, [1, 2]);
// no await waterfall / inert derived warnings
assert.deepEqual(warnings, []);
}
});

@ -0,0 +1,31 @@
<script>
import Child from "./Child.svelte";
let count = $state(0);
let deferreds = [];
function push(v) {
return new Promise((resolve, reject) => {
deferreds.push({ resolve: () => resolve(v), reject });
});
}
</script>
<button onclick={() => count += 1}>increment</button>
<button onclick={() => deferreds.shift()?.resolve()}>resolve</button>
<svelte:boundary>
{#if count % 2 === 0}
{@const double = count * 2}
<p>true</p>
{await push(count)} {double}
<Child count={await push(count)} />
{:else}
<p>false</p>
<Child count={await push(count)} />
{/if}
{#snippet pending()}
<p>loading...</p>
{/snippet}
</svelte:boundary>

@ -0,0 +1,24 @@
import { tick } from 'svelte';
import { test } from '../../test';
import { normalise_trace_logs } from '../../../helpers.js';
export default test({
compileOptions: {
dev: true
},
html: '<p>pending</p>',
async test({ assert, target, warnings }) {
await tick();
assert.htmlEqual(
target.innerHTML,
'<h1>number -> number -> number -> return -> body failed -> ended</h1>'
);
assert.deepEqual(normalise_trace_logs(warnings), [
{
log: 'Detected reactivity loss when reading `values[1]`. This happens when state is read in an async function after an earlier `await`'
}
]);
}
});

@ -0,0 +1,45 @@
<script>
let values = $state([0, 1, 2]);
async function get_result() {
const logs = [];
const iterator = {
index: 0,
async next() {
if (this.index > 2) { done: true }
return { done: false, value: values[this.index++] };
},
async return() {
logs.push('return');
},
[Symbol.asyncIterator]() {
return this;
}
};
try {
for await (const value of iterator) {
logs.push('number');
// Read reactive state after async iterator await.
if (values.length === 3 && value === 2) {
throw new Error('body failed');
}
}
logs.push('done');
} catch (error) {
logs.push(error.message);
}
logs.push('ended');
return logs.join(' -> ');
}
</script>
<svelte:boundary>
<h1>{await get_result()}</h1>
{#snippet pending()}
<p>pending</p>
{/snippet}
</svelte:boundary>

@ -0,0 +1,21 @@
import { tick } from 'svelte';
import { test } from '../../test';
import { normalise_trace_logs } from '../../../helpers.js';
export default test({
compileOptions: {
dev: true
},
html: '<p>pending</p>',
async test({ assert, target, warnings }) {
await tick();
assert.htmlEqual(target.innerHTML, '<h1>number -> number -> next failed -> ended</h1>');
assert.deepEqual(normalise_trace_logs(warnings), [
{
log: 'Detected reactivity loss when reading `values[1]`. This happens when state is read in an async function after an earlier `await`'
}
]);
}
});

@ -0,0 +1,43 @@
<script>
let values = $state([0, 1, 2]);
async function get_result() {
const logs = [];
const iterator = {
index: 0,
async next() {
if (this.index > 1) throw new Error('next failed');
return { done: false, value: values[this.index++] };
},
async return() {
logs.push('return');
},
[Symbol.asyncIterator]() {
return this;
}
};
try {
for await (const value of iterator) {
logs.push('number');
// Read reactive state after async iterator await.
values.length === value;
}
logs.push('done');
} catch (error) {
logs.push(error.message);
}
logs.push('ended');
return logs.join(' -> ');
}
</script>
<svelte:boundary>
<h1>{await get_result()}</h1>
{#snippet pending()}
<p>pending</p>
{/snippet}
</svelte:boundary>

@ -0,0 +1,7 @@
<script>
import { push } from "./main.svelte";
const x = await push(1);
const y = await push(2);
</script>
{x} {y}

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

@ -0,0 +1,23 @@
<script module>
const queued = [];
export function push(v) {
return new Promise((fulfil) => {
queued.push(() => fulfil(v));
});
}
</script>
<script>
import Child from "./Child.svelte";
let show = $state(false);
let count = $state(0);
</script>
{#if show}
<Child />
{/if}
<button onclick={() => show = true}>show</button>
<button onclick={() => queued.shift()?.()}>resolve</button>
<button onclick={() => count++}>{count}</button>

@ -0,0 +1,28 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target }) {
await tick();
const [increment, hide, pop] = target.querySelectorAll('button');
increment.click();
await tick();
pop.click();
await tick();
hide.click(); // hides the if block, which cancels the pending async inside, which means the batch can complete
await tick();
assert.htmlEqual(
target.innerHTML,
`<button>increment</button> <button>hide</button> <button>pop</button> 1`
);
pop.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`<button>increment</button> <button>hide</button> <button>pop</button> 1`
);
}
});

@ -0,0 +1,21 @@
<script>
let show = $state(true);
let count = $state(0);
const queue = [];
function push(value) {
if (!value) return value;
return new Promise(r => queue.push(() => r(value)));
}
</script>
<button onclick={() => count += 1}>increment</button>
<button onclick={() => show = false}>hide</button>
<!-- pop() so that the outer one resolves first, not the one inside the if block -->
<button onclick={() => queue.pop()?.()}>pop</button>
{await push(count)}
{#if show}
{await push(count)}
{/if}

@ -0,0 +1,33 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target }) {
await tick();
const [increment, shift] = target.querySelectorAll('button');
increment.click();
await tick();
increment.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`<button>3</button><button>shift</button><p>1 = 1</p><p>fizz: true</p><p>buzz: true</p>`
);
shift.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`<button>3</button><button>shift</button><p>1 = 1</p><p>fizz: true</p><p>buzz: true</p>`
);
shift.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`<button>3</button><button>shift</button><p>3 = 3</p><p>fizz: true</p><p>buzz: false</p>`
);
}
});

@ -0,0 +1,44 @@
<script>
import { getAbortSignal } from 'svelte';
const queue = [];
let n = $state(1);
let fizz = $state(true);
let buzz = $state(true);
function increment() {
n++;
fizz = n % 3 === 0;
buzz = n % 5 === 0;
}
function push(value) {
if (value === 1) return 1;
const d = Promise.withResolvers();
queue.push(() => d.resolve(value));
const signal = getAbortSignal();
signal.onabort = () => d.reject(signal.reason);
return d.promise;
}
</script>
<button onclick={increment}>
{$state.eager(n)}
</button>
<button onclick={() => queue.shift()?.()}>shift</button>
<p>{n} = {await push(n)}</p>
{#if true}
<p>fizz: {fizz}</p>
{/if}
{#if true}
<p>buzz: {buzz}</p>
{/if}

@ -0,0 +1,34 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target }) {
await tick();
const [button1, button2, pop, shift] = target.querySelectorAll('button');
const [p] = target.querySelectorAll('p');
button1.click();
await tick();
button2.click();
await tick();
assert.htmlEqual(p.innerHTML, `0 + 0 = 0 | 0 0`);
shift.click();
await tick();
assert.htmlEqual(p.innerHTML, `0 + 0 = 0 | 0 0`);
pop.click();
await tick();
assert.htmlEqual(p.innerHTML, `0 + 0 = 0 | 0 0`);
pop.click();
await tick();
assert.htmlEqual(p.innerHTML, `1 + 0 = 1 | 1 0`);
shift.click();
await tick();
pop.click();
await tick();
assert.htmlEqual(p.innerHTML, `1 + 2 = 3 | 1 1`);
}
});

@ -0,0 +1,21 @@
<script>
const queue1 = [];
const queue2 = [];
let a = $state(0);
let b = $state(0);
let c = $state(0);
let d = $state(0)
function push(value, where = 1) {
if (!value) return value;
return new Promise(r => (where === 1 ? queue1 : queue2).push(() => r(value)));
}
</script>
<button onclick={() => {a++;c++}}>a / c</button>
<button onclick={() => {b+=2;d++}}>b / d</button>
<button onclick={() => queue1.pop()?.()}>pop 1</button>
<button onclick={() => queue2.shift()?.()}>shift 2</button>
<p>{a} + {b} = {await push(a + b)} | {await push(c, 2)} {await push(d, 2)}</p>

@ -0,0 +1,34 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target }) {
await tick();
const [button1, button2, shift_1, pop_1, shift_2] = target.querySelectorAll('button');
const [p] = target.querySelectorAll('p');
button1.click();
await tick();
button2.click();
await tick();
assert.htmlEqual(p.innerHTML, `0 + 0 = 0 | 0 0`);
pop_1.click();
await tick();
shift_2.click();
await tick();
assert.htmlEqual(p.innerHTML, `0 + 0 = 0 | 0 0`);
// Check that the first batch can still resolve before the second even if one of its async values
// is already superseeded (but the subsequent batch as a whole is still pending).
shift_1.click();
await tick();
assert.htmlEqual(p.innerHTML, `1 + 0 = 1 | 1 0`);
shift_1.click();
await tick();
shift_2.click();
await tick();
assert.htmlEqual(p.innerHTML, `1 + 2 = 3 | 1 1`);
}
});

@ -0,0 +1,22 @@
<script>
const queue1 = [];
const queue2 = [];
let a = $state(0);
let b = $state(0);
let c = $state(0);
let d = $state(0)
function push(value, where = 1) {
if (!value) return value;
return new Promise(r => (where === 1 ? queue1 : queue2).push(() => r(value)));
}
</script>
<button onclick={() => {a++;c++}}>a / c</button>
<button onclick={() => {b+=2;d++}}>b / d</button>
<button onclick={() => queue1.shift()?.()}>shift 1</button>
<button onclick={() => queue1.pop()?.()}>pop 1</button>
<button onclick={() => queue2.shift()?.()}>shift 2</button>
<p>{a} + {b} = {await push(a + b)} | {await push(c, 2)} {await push(d, 2)}</p>

@ -0,0 +1,20 @@
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();
increment.click();
await tick();
pop.click();
await tick();
assert.htmlEqual(target.innerHTML, '<button>increment</button><button>pop</button> 2 2 1'); // showing nothing here yet would also be ok
pop.click();
await tick();
assert.htmlEqual(target.innerHTML, '<button>increment</button><button>pop</button> 2 2 1');
}
});

@ -0,0 +1,23 @@
<script>
let count = $state(0);
let other = $state(0);
const queue = [];
function push(v) {
if (v === 0) return v;
return new Promise((resolve) => queue.push(() => resolve(v)));
}
</script>
<button onclick={() => {
if (count === 0) other++;
count++;
}}>increment</button>
<button onclick={() => queue.pop()?.()}>pop</button>
{#if count > 0}
{await push(count)} {count} {other}
{/if}

@ -0,0 +1,21 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
compileOptions: { dev: true }, // for testing that teardown effect in eager $.get(loaded) doesn't lead to a crash (because it means REACTION_RAN is set, which means unfreeze_derived runs)
async test({ assert, target }) {
const [count, shift] = target.querySelectorAll('button');
shift.click();
await tick();
assert.htmlEqual(target.innerHTML, `<button>0</button><button>shift</button><p>0</p>`);
count.click();
await tick();
assert.htmlEqual(target.innerHTML, `<button>1</button><button>shift</button><p>0 (...)</p>`);
shift.click();
await tick();
assert.htmlEqual(target.innerHTML, `<button>1</button><button>shift</button><p>1</p>`);
}
});

@ -0,0 +1,21 @@
<script>
let count = $state(0);
let resolvers = [];
function push(value) {
const { promise, resolve } = Promise.withResolvers();
resolvers.push(() => resolve(value));
return promise;
}
</script>
<button onclick={() => count += 1}>{$state.eager(count)}</button>
<button onclick={() => resolvers.shift()?.()}>shift</button>
<svelte:boundary>
{@const loaded = $state.eager(count) === count}
<p>{await push(count)} {loaded ? '' : '(...)'}</p>
{#snippet pending()}{/snippet}
</svelte:boundary>

@ -0,0 +1,8 @@
import { test } from '../../test';
export default test({
mode: ['client', 'server'],
html: `<span>x</span>`,
ssrHtml: `<span>x</span>`
});

@ -0,0 +1,8 @@
<script>
import Icon from './Icon.svelte';
let icons = $state({ currency: { Icon } });
const platformIcons = $derived(icons);
</script>
<platformIcons.currency.Icon />

@ -0,0 +1,54 @@
import { tick } from 'svelte';
import { ok, test } from '../../test';
// Test that the store is unsubscribed from, even if it's not referenced once the store itself is set to null
export default test({
skip_no_async: true,
async test({ target, assert }) {
assert.htmlEqual(
target.innerHTML,
`<input type="number"> <p>0</p> <button>add watcher</button>`
);
target.querySelector('button')?.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`<input type="number"> <p>1</p> hello 1 <button>remove watcher</button>`
);
const input = target.querySelector('input');
ok(input);
input.stepUp();
input.dispatchEvent(new Event('input', { bubbles: true }));
await tick();
assert.htmlEqual(
target.innerHTML,
`<input type="number"> <p>2</p> hello 2 <button>remove watcher</button>`
);
target.querySelector('button')?.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`<input type="number"> <p>2</p> <button>add watcher</button>`
);
input.stepUp();
input.dispatchEvent(new Event('input', { bubbles: true }));
await tick();
assert.htmlEqual(
target.innerHTML,
`<input type="number"> <p>2</p> <button>add watcher</button>`
);
target.querySelector('button')?.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`<input type="number"> <p>3</p> hello 3 <button>remove watcher</button>`
);
}
});

@ -0,0 +1,29 @@
<script>
import { writable, derived } from "svelte/store";
const obj = writable({ a: 1 });
let count = $state(0);
let watcherA = $state();
function watch (prop) {
return derived(obj, (o) => {
count++;
return o[prop];
});
}
</script>
<input type="number" bind:value={$obj.a}>
<p>{count}</p>
{#if watcherA}
<!-- make sure the presence of async work doesn't break the `legacy_updates` mechanism -->
{#if true}
{await 'hello'}
{/if}
{$watcherA}
<button on:click={() => watcherA = null}>remove watcher</button>
{:else}
<button on:click={() => watcherA = watch("a")}>add watcher</button>
{/if}

@ -0,0 +1,10 @@
<script>
import { fly } from 'svelte/transition';
// Not read before the outro, by which time the derived is destroyed
let duration = $derived(10);
</script>
<div in:fly={{ duration: 10 }} out:fly={{ duration }}>
hello
</div>

@ -0,0 +1,32 @@
import { flushSync } from 'svelte';
import { raf } from '../../../animation-helpers';
import { test } from '../../test';
export default test({
async test({ assert, target }) {
const [fly_in, fly_out] = target.querySelectorAll('button');
fly_in.click();
flushSync();
raf.tick(25);
assert.htmlEqual(
target.innerHTML,
`
<button>fly in</button>
<button>fly out</button>
<div style="">hello</div>
`
);
fly_out.click();
flushSync();
raf.tick(50);
assert.htmlEqual(
target.innerHTML,
`
<button>fly in</button>
<button>fly out</button>
`
);
}
});

@ -0,0 +1,11 @@
<script>
import Child from './Child.svelte';
let show = $state(false);
</script>
<button onclick={() => (show = true)}>fly in</button>
<button onclick={() => (show = false)}>fly out</button>
{#if show}
<Child />
{/if}

@ -0,0 +1,8 @@
import { test } from '../../test';
export default test({
mode: ['async'],
compileOptions: {
dev: true
}
});

@ -0,0 +1 @@
<!--1410iyz--><!----><title>Async multiple attributes</title>

@ -0,0 +1,15 @@
<script>
const user = $derived(Promise.resolve({
name: 'test',
image: '',
}))
</script>
<svelte:head>
<title>Async multiple attributes</title>
</svelte:head>
<img
alt={(await user).name}
src={(await user).image}
/>

@ -5,7 +5,12 @@
onclick: 'alert(1)',
onerror: 'alert(1)',
onfocus: 'alert(1)',
onmouseover: 'alert(1)'
onmouseover: 'alert(1)',
' onload': 'alert(1)',
'\tonload': 'alert(1)',
'\u00a0onload': 'alert(1)',
"\t": "/onmouseover=alert(1)//",
"": "/onmouseover=alert(1)//"
};
</script>

@ -1,9 +1,9 @@
import 'svelte/internal/disclose-version';
import * as $ from 'svelte/internal/client';
var root = $.from_html(`<h1></h1> <b></b> <button> </button> <h1></h1>`, 1);
var root = $.from_html(`<h1></h1> <b></b> <button> </button> <h1></h1> <div></div>`, 1);
export default function Nullish_coallescence_omittance($$anchor) {
export default function Nullish_coallescence_omittance($$anchor, $$props) {
let name = 'world';
let count = $.state(0);
var fragment = root();
@ -23,7 +23,14 @@ export default function Nullish_coallescence_omittance($$anchor) {
var h1_1 = $.sibling(button, 2);
h1_1.textContent = 'Hello, world';
$.template_effect(() => $.set_text(text, `Count is ${$.get(count) ?? ''}`));
var div = $.sibling(h1_1, 2);
$.template_effect(() => {
$.set_text(text, `Count is ${$.get(count) ?? ''}`);
$.set_attribute(div, 'title', `Hello, world ${$.get(count) ?? ''} 1 ${typeof $$props.value} ${$$props.value ?? ''}`);
});
$.delegated('click', button, () => $.update(count));
$.append($$anchor, fragment);
}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save