Merge branch 'elliott/effect-pending-correct-boundary' into adjust-boundary-error-message

pull/16762/head
S. Elliott Johnson 1 month ago
commit 380f45725b

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: send `$effect.pending` count to the correct boundary

@ -1,5 +0,0 @@
---
'svelte': patch
---
fix: properly catch top level await errors

@ -160,9 +160,9 @@ export function logger(getValue) {
### Component testing
It is possible to test your components in isolation using Vitest.
It is possible to test your components in isolation, which allows you to render them in a browser (real or simulated), simulate behavior, and make assertions, without spinning up your whole app.
> [!NOTE] Before writing component tests, think about whether you actually need to test the component, or if it's more about the logic _inside_ the component. If so, consider extracting out that logic to test it in isolation, without the overhead of a component
> [!NOTE] Before writing component tests, think about whether you actually need to test the component, or if it's more about the logic _inside_ the component. If so, consider extracting out that logic to test it in isolation, without the overhead of a component.
To get started, install jsdom (a library that shims DOM APIs):
@ -246,6 +246,48 @@ test('Component', async () => {
When writing component tests that involve two-way bindings, context or snippet props, it's best to create a wrapper component for your specific test and interact with that. `@testing-library/svelte` contains some [examples](https://testing-library.com/docs/svelte-testing-library/example).
### Component testing with Storybook
[Storybook](https://storybook.js.org) is a tool for developing and documenting UI components, and it can also be used to test your components. They're run with Vitest's browser mode, which renders your components in a real browser for the most realistic testing environment.
To get started, first install Storybook ([using Svelte's CLI](/docs/cli/storybook)) in your project via `npx sv add storybook` and choose the recommended configuration that includes testing features. If you're already using Storybook, and for more information on Storybook's testing capabilities, follow the [Storybook testing docs](https://storybook.js.org/docs/writing-tests?renderer=svelte) to get started.
You can create stories for component variations and test interactions with the [play function](https://storybook.js.org/docs/writing-tests/interaction-testing?renderer=svelte#writing-interaction-tests), which allows you to simulate behavior and make assertions using the Testing Library and Vitest APIs. Here's an example of two stories that can be tested, one that renders an empty LoginForm component and one that simulates a user filling out the form:
```svelte
/// file: LoginForm.stories.svelte
<script module>
import { defineMeta } from '@storybook/addon-svelte-csf';
import { expect, fn } from 'storybook/test';
import LoginForm from './LoginForm.svelte';
const { Story } = defineMeta({
component: LoginForm,
args: {
// Pass a mock function to the `onSubmit` prop
onSubmit: fn(),
}
});
</script>
<Story name="Empty Form" />
<Story
name="Filled Form"
play={async ({ args, canvas, userEvent }) => {
// Simulate a user filling out the form
await userEvent.type(canvas.getByTestId('email'), 'email@provider.com');
await userEvent.type(canvas.getByTestId('password'), 'a-random-password');
await userEvent.click(canvas.getByRole('button'));
// Run assertions
await expect(args.onSubmit).toHaveBeenCalledTimes(1);
await expect(canvas.getByText('Youre in!')).toBeInTheDocument();
}}
/>
```
## E2E tests using Playwright
E2E (short for 'end to end') tests allow you to test your full application through the eyes of the user. This section uses [Playwright](https://playwright.dev/) as an example, but you can also use other solutions like [Cypress](https://www.cypress.io/) or [NightwatchJS](https://nightwatchjs.org/).

@ -4,7 +4,7 @@ title: Custom elements
<!-- - [basically what we have today](https://svelte.dev/docs/custom-elements-api) -->
Svelte components can also be compiled to custom elements (aka web components) using the `customElement: true` compiler option. You should specify a tag name for the component using the `<svelte:options>` [element](svelte-options).
Svelte components can also be compiled to custom elements (aka web components) using the `customElement: true` compiler option. You should specify a tag name for the component using the `<svelte:options>` [element](svelte-options). Within the custom element you can access the host element via the [`$host`](https://svelte.dev/docs/svelte/$host) rune.
```svelte
<svelte:options customElement="my-element" />

@ -196,6 +196,51 @@ Cyclical dependency detected: %cycle%
`{@const}` must be the immediate child of `{#snippet}`, `{#if}`, `{:else if}`, `{:else}`, `{#each}`, `{:then}`, `{:catch}`, `<svelte:fragment>`, `<svelte:boundary` or `<Component>`
```
### const_tag_invalid_reference
```
The `{@const %name% = ...}` declaration is not available in this snippet
```
The following is an error:
```svelte
<svelte:boundary>
{@const foo = 'bar'}
{#snippet failed()}
{foo}
{/snippet}
</svelte:boundary>
```
Here, `foo` is not available inside `failed`. The top level code inside `<svelte:boundary>` becomes part of the implicit `children` snippet, in other words the above code is equivalent to this:
```svelte
<svelte:boundary>
{#snippet children()}
{@const foo = 'bar'}
{/snippet}
{#snippet failed()}
{foo}
{/snippet}
</svelte:boundary>
```
The same applies to components:
```svelte
<Component>
{@const foo = 'bar'}
{#snippet someProp()}
<!-- error -->
{foo}
{/snippet}
</Component>
```
### constant_assignment
```

@ -1,5 +1,55 @@
# svelte
## 5.38.7
### Patch Changes
- fix: replace `undefined` with `void(0)` in CallExpressions ([#16693](https://github.com/sveltejs/svelte/pull/16693))
- fix: ensure batch exists when resetting a failed boundary ([#16698](https://github.com/sveltejs/svelte/pull/16698))
- fix: place store setup inside async body ([#16687](https://github.com/sveltejs/svelte/pull/16687))
## 5.38.6
### Patch Changes
- fix: don't fail on `flushSync` while flushing effects ([#16674](https://github.com/sveltejs/svelte/pull/16674))
## 5.38.5
### Patch Changes
- fix: ensure async deriveds always get dependencies from thennable ([#16672](https://github.com/sveltejs/svelte/pull/16672))
## 5.38.4
### Patch Changes
- fix: place instance-level snippets inside async body ([#16666](https://github.com/sveltejs/svelte/pull/16666))
- fix: Add check for builtin custom elements in `set_custom_element_data` ([#16592](https://github.com/sveltejs/svelte/pull/16592))
- fix: restore batch along with effect context ([#16668](https://github.com/sveltejs/svelte/pull/16668))
- fix: wait until changes propagate before updating input selection state ([#16649](https://github.com/sveltejs/svelte/pull/16649))
- fix: add "Accept-CH" as valid value for `http-equiv` ([#16671](https://github.com/sveltejs/svelte/pull/16671))
## 5.38.3
### Patch Changes
- fix: ensure correct order of template effect values ([#16655](https://github.com/sveltejs/svelte/pull/16655))
- fix: allow async `{@const}` in more places ([#16643](https://github.com/sveltejs/svelte/pull/16643))
- fix: properly catch top level await errors ([#16619](https://github.com/sveltejs/svelte/pull/16619))
- perf: prune effects without dependencies ([#16625](https://github.com/sveltejs/svelte/pull/16625))
- fix: only emit `for_await_track_reactivity_loss` in async mode ([#16644](https://github.com/sveltejs/svelte/pull/16644))
## 5.38.2
### Patch Changes

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

@ -124,6 +124,49 @@
> `{@const}` must be the immediate child of `{#snippet}`, `{#if}`, `{:else if}`, `{:else}`, `{#each}`, `{:then}`, `{:catch}`, `<svelte:fragment>`, `<svelte:boundary` or `<Component>`
## const_tag_invalid_reference
> The `{@const %name% = ...}` declaration is not available in this snippet
The following is an error:
```svelte
<svelte:boundary>
{@const foo = 'bar'}
{#snippet failed()}
{foo}
{/snippet}
</svelte:boundary>
```
Here, `foo` is not available inside `failed`. The top level code inside `<svelte:boundary>` becomes part of the implicit `children` snippet, in other words the above code is equivalent to this:
```svelte
<svelte:boundary>
{#snippet children()}
{@const foo = 'bar'}
{/snippet}
{#snippet failed()}
{foo}
{/snippet}
</svelte:boundary>
```
The same applies to components:
```svelte
<Component>
{@const foo = 'bar'}
{#snippet someProp()}
<!-- error -->
{foo}
{/snippet}
</Component>
```
## debug_tag_invalid_arguments
> {@debug ...} arguments must be identifiers, not arbitrary expressions

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

@ -985,6 +985,16 @@ export function const_tag_invalid_placement(node) {
e(node, 'const_tag_invalid_placement', `\`{@const}\` must be the immediate child of \`{#snippet}\`, \`{#if}\`, \`{:else if}\`, \`{:else}\`, \`{#each}\`, \`{:then}\`, \`{:catch}\`, \`<svelte:fragment>\`, \`<svelte:boundary\` or \`<Component>\`\nhttps://svelte.dev/e/const_tag_invalid_placement`);
}
/**
* The `{@const %name% = ...}` declaration is not available in this snippet
* @param {null | number | NodeLike} node
* @param {string} name
* @returns {never}
*/
export function const_tag_invalid_reference(node, name) {
e(node, 'const_tag_invalid_reference', `The \`{@const ${name} = ...}\` declaration is not available in this snippet \nhttps://svelte.dev/e/const_tag_invalid_reference`);
}
/**
* {@debug ...} arguments must be identifiers, not arbitrary expressions
* @param {null | number | NodeLike} node

@ -7,6 +7,7 @@ import * as w from '../../../warnings.js';
import { is_rune } from '../../../../utils.js';
import { mark_subtree_dynamic } from './shared/fragment.js';
import { get_rune } from '../../scope.js';
import { is_component_node } from '../../nodes.js';
/**
* @param {Identifier} node
@ -155,5 +156,37 @@ export function Identifier(node, context) {
) {
w.reactive_declaration_module_script_dependency(node);
}
if (binding.metadata?.is_template_declaration && context.state.options.experimental.async) {
let snippet_name;
// Find out if this references a {@const ...} declaration of an implicit children snippet
// when it is itself inside a snippet block at the same level. If so, error.
for (let i = context.path.length - 1; i >= 0; i--) {
const parent = context.path[i];
const grand_parent = context.path[i - 1];
if (parent.type === 'SnippetBlock') {
snippet_name = parent.expression.name;
} else if (
snippet_name &&
grand_parent &&
parent.type === 'Fragment' &&
(is_component_node(grand_parent) ||
(grand_parent.type === 'SvelteBoundary' &&
(snippet_name === 'failed' || snippet_name === 'pending')))
) {
if (
is_component_node(grand_parent)
? grand_parent.metadata.scopes.default === binding.scope
: context.state.scopes.get(parent) === binding.scope
) {
e.const_tag_invalid_reference(node, node.name);
} else {
break;
}
}
}
}
}
}

@ -360,22 +360,41 @@ export function client_component(analysis, options) {
let component_block = b.block([
store_init,
...store_setup,
...legacy_reactive_declarations,
...group_binding_declarations,
...state.instance_level_snippets
...group_binding_declarations
]);
const should_inject_context =
dev ||
analysis.needs_context ||
analysis.reactive_statements.size > 0 ||
component_returned_object.length > 0;
if (analysis.instance.has_await) {
if (should_inject_context && component_returned_object.length > 0) {
component_block.body.push(b.var('$$exports'));
}
const body = b.block([
...store_setup,
...state.instance_level_snippets,
.../** @type {ESTree.Statement[]} */ (instance.body),
...(should_inject_context && component_returned_object.length > 0
? [b.stmt(b.assignment('=', b.id('$$exports'), b.object(component_returned_object)))]
: []),
b.if(b.call('$.aborted'), b.return()),
.../** @type {ESTree.Statement[]} */ (template.body)
]);
component_block.body.push(b.stmt(b.call(`$.async_body`, b.arrow([], body, true))));
} else {
component_block.body.push(.../** @type {ESTree.Statement[]} */ (instance.body));
component_block.body.push(
...state.instance_level_snippets,
.../** @type {ESTree.Statement[]} */ (instance.body)
);
if (should_inject_context && component_returned_object.length > 0) {
component_block.body.push(b.var('$$exports', b.object(component_returned_object)));
}
component_block.body.unshift(...store_setup);
if (!analysis.runes && analysis.needs_context) {
component_block.body.push(b.stmt(b.call('$.init', analysis.immutable ? b.true : undefined)));
@ -390,12 +409,6 @@ export function client_component(analysis, options) {
);
}
const should_inject_context =
dev ||
analysis.needs_context ||
analysis.reactive_statements.size > 0 ||
component_returned_object.length > 0;
let should_inject_props =
should_inject_context ||
analysis.needs_props ||
@ -442,7 +455,7 @@ export function client_component(analysis, options) {
let to_push;
if (component_returned_object.length > 0) {
let pop_call = b.call('$.pop', b.object(component_returned_object));
let pop_call = b.call('$.pop', b.id('$$exports'));
to_push = needs_store_cleanup ? b.var('$$pop', pop_call) : b.return(pop_call);
} else {
to_push = b.stmt(b.call('$.pop'));
@ -453,6 +466,7 @@ export function client_component(analysis, options) {
if (needs_store_cleanup) {
component_block.body.push(b.stmt(b.call('$$cleanup')));
if (component_returned_object.length > 0) {
component_block.body.push(b.return(b.id('$$pop')));
}

@ -8,7 +8,12 @@ import { dev, is_ignored } from '../../../../state.js';
* @param {ComponentContext} context
*/
export function ForOfStatement(node, context) {
if (node.await && dev && !is_ignored(node, 'await_reactivity_loss')) {
if (
node.await &&
dev &&
!is_ignored(node, 'await_reactivity_loss') &&
context.state.options.experimental.async
) {
const left = /** @type {VariableDeclaration | Pattern} */ (context.visit(node.left));
const argument = /** @type {Expression} */ (context.visit(node.right));
const body = /** @type {Statement} */ (context.visit(node.body));

@ -51,7 +51,6 @@ export function Fragment(node, context) {
const has_await = context.state.init !== null && (node.metadata.has_await || false);
const template_name = context.state.scope.root.unique('root'); // TODO infer name from parent
const unsuspend = b.id('$$unsuspend');
/** @type {Statement[]} */
const body = [];
@ -151,10 +150,6 @@ export function Fragment(node, context) {
}
}
if (has_await) {
body.push(b.var(unsuspend, b.call('$.suspend')));
}
body.push(...state.consts);
if (has_await) {
@ -182,8 +177,8 @@ export function Fragment(node, context) {
}
if (has_await) {
body.push(b.stmt(b.call(unsuspend)));
return b.block([b.stmt(b.call('$.async_body', b.arrow([], b.block(body), true)))]);
} else {
return b.block(body);
}
return b.block(body);
}

@ -39,40 +39,60 @@ export function SvelteBoundary(node, context) {
/** @type {Statement[]} */
const hoisted = [];
let has_const = false;
// const tags need to live inside the boundary, but might also be referenced in hoisted snippets.
// to resolve this we cheat: we duplicate const tags inside snippets
// We'll revert this behavior in the future, it was a mistake to allow this (Component snippets also don't do this).
for (const child of node.fragment.nodes) {
if (child.type === 'ConstTag') {
context.visit(child, { ...context.state, consts: const_tags });
has_const = true;
if (!context.state.options.experimental.async) {
context.visit(child, { ...context.state, consts: const_tags });
}
}
}
for (const child of node.fragment.nodes) {
if (child.type === 'ConstTag') {
if (context.state.options.experimental.async) {
nodes.push(child);
}
continue;
}
if (child.type === 'SnippetBlock') {
/** @type {Statement[]} */
const statements = [];
context.visit(child, { ...context.state, init: statements });
const snippet = /** @type {VariableDeclaration} */ (statements[0]);
const snippet_fn = dev
? // @ts-expect-error we know this shape is correct
snippet.declarations[0].init.arguments[1]
: snippet.declarations[0].init;
snippet_fn.body.body.unshift(
...const_tags.filter((node) => node.type === 'VariableDeclaration')
);
hoisted.push(snippet);
if (['failed', 'pending'].includes(child.expression.name)) {
props.properties.push(b.prop('init', child.expression, child.expression));
if (
context.state.options.experimental.async &&
has_const &&
!['failed', 'pending'].includes(child.expression.name)
) {
// we can't hoist snippets as they may reference const tags, so we just keep them in the fragment
nodes.push(child);
} else {
/** @type {Statement[]} */
const statements = [];
context.visit(child, { ...context.state, init: statements });
const snippet = /** @type {VariableDeclaration} */ (statements[0]);
const snippet_fn = dev
? // @ts-expect-error we know this shape is correct
snippet.declarations[0].init.arguments[1]
: snippet.declarations[0].init;
if (!context.state.options.experimental.async) {
snippet_fn.body.body.unshift(
...const_tags.filter((node) => node.type === 'VariableDeclaration')
);
}
if (['failed', 'pending'].includes(child.expression.name)) {
props.properties.push(b.prop('init', child.expression, child.expression));
}
hoisted.push(snippet);
}
continue;
@ -83,7 +103,9 @@ export function SvelteBoundary(node, context) {
const block = /** @type {BlockStatement} */ (context.visit({ ...node.fragment, nodes }));
block.body.unshift(...const_tags);
if (!context.state.options.experimental.async) {
block.body.unshift(...const_tags);
}
const boundary = b.stmt(
b.call('$.boundary', context.state.node, props, b.arrow([b.id('$$anchor')], block))

@ -34,7 +34,7 @@ export class Memoizer {
}
apply() {
return [...this.#async, ...this.#sync].map((memo, i) => {
return [...this.#sync, ...this.#async].map((memo, i) => {
memo.id.name = `$${i}`;
return memo.id;
});

@ -23,6 +23,15 @@ export function is_element_node(node) {
return element_nodes.includes(node.type);
}
/**
* Returns true for all component-like nodes
* @param {AST.SvelteNode} node
* @returns {node is AST.Component | AST.SvelteComponent | AST.SvelteSelf}
*/
export function is_component_node(node) {
return ['Component', 'SvelteComponent', 'SvelteSelf'].includes(node.type);
}
/**
* @param {AST.RegularElement | AST.SvelteElement} node
* @returns {boolean}

@ -122,7 +122,7 @@ export class Binding {
/**
* Additional metadata, varies per binding type
* @type {null | { inside_rest?: boolean }}
* @type {null | { inside_rest?: boolean; is_template_declaration?: boolean }}
*/
metadata = null;
@ -1121,6 +1121,7 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
node.kind,
declarator.init
);
binding.metadata = { is_template_declaration: true };
bindings.push(binding);
}
}

@ -100,7 +100,7 @@ export function call(callee, ...args) {
if (typeof callee === 'string') callee = id(callee);
args = args.slice();
// replacing missing arguments with `undefined`, unless they're at the end in which case remove them
// replacing missing arguments with `void(0)`, unless they're at the end in which case remove them
let i = args.length;
let popping = true;
while (i--) {
@ -108,7 +108,7 @@ export function call(callee, ...args) {
if (popping) {
args.pop();
} else {
args[i] = id('undefined');
args[i] = void0;
}
} else {
popping = false;

@ -393,6 +393,9 @@ export class Boundary {
e.svelte_boundary_reset_onerror();
}
// If the failure happened while flushing effects, current_batch can be null
Batch.ensure();
// this ensures we modify the cascading_pending_count of the correct parent
// by the number we're decreasing this boundary by
this.update_pending_count(-this.#pending_count, true);

@ -238,10 +238,10 @@ export function set_custom_element_data(node, prop, value) {
// Don't compute setters for custom elements while they aren't registered yet,
// because during their upgrade/instantiation they might add more setters.
// Instead, fall back to a simple "an object, then set as property" heuristic.
(setters_cache.has(node.nodeName) ||
(setters_cache.has(node.getAttribute('is') || node.nodeName) ||
// customElements may not be available in browser extension contexts
!customElements ||
customElements.get(node.tagName.toLowerCase())
customElements.get(node.getAttribute('is') || node.tagName.toLowerCase())
? get_setters(node).includes(prop)
: value && typeof value === 'object')
) {
@ -546,9 +546,10 @@ var setters_cache = new Map();
/** @param {Element} element */
function get_setters(element) {
var setters = setters_cache.get(element.nodeName);
var cache_key = element.getAttribute('is') || element.nodeName;
var setters = setters_cache.get(cache_key);
if (setters) return setters;
setters_cache.set(element.nodeName, (setters = []));
setters_cache.set(cache_key, (setters = []));
var descriptors;
var proto = element; // In the case of custom elements there might be setters on the instance

@ -6,7 +6,7 @@ import * as e from '../../../errors.js';
import { is } from '../../../proxy.js';
import { queue_micro_task } from '../../task.js';
import { hydrating } from '../../hydration.js';
import { untrack } from '../../../runtime.js';
import { tick, untrack } from '../../../runtime.js';
import { is_runes } from '../../../context.js';
import { current_batch, previous_batch } from '../../../reactivity/batch.js';
@ -17,11 +17,9 @@ import { current_batch, previous_batch } from '../../../reactivity/batch.js';
* @returns {void}
*/
export function bind_value(input, get, set = get) {
var runes = is_runes();
var batches = new WeakSet();
listen_to_event_and_reset_event(input, 'input', (is_reset) => {
listen_to_event_and_reset_event(input, 'input', async (is_reset) => {
if (DEV && input.type === 'checkbox') {
// TODO should this happen in prod too?
e.bind_invalid_checkbox_value();
@ -36,9 +34,13 @@ export function bind_value(input, get, set = get) {
batches.add(current_batch);
}
// In runes mode, respect any validation in accessors (doesn't apply in legacy mode,
// because we use mutable state which ensures the render effect always runs)
if (runes && value !== (value = get())) {
// Because `{#each ...}` blocks work by updating sources inside the flush,
// we need to wait a tick before checking to see if we should forcibly
// update the input and reset the selection state
await tick();
// Respect any validation in accessors
if (value !== (value = get())) {
var start = input.selectionStart;
var end = input.selectionEnd;

@ -71,11 +71,13 @@ function capture() {
var previous_effect = active_effect;
var previous_reaction = active_reaction;
var previous_component_context = component_context;
var previous_batch = current_batch;
return function restore() {
set_active_effect(previous_effect);
set_active_reaction(previous_reaction);
set_component_context(previous_component_context);
previous_batch?.activate();
if (DEV) {
set_from_async_derived(null);
@ -174,8 +176,8 @@ export function unset_context() {
* @param {() => Promise<void>} fn
*/
export async function async_body(fn) {
const unsuspend = suspend();
const active = /** @type {Effect} */ (active_effect);
var unsuspend = suspend();
var active = /** @type {Effect} */ (active_effect);
try {
await fn();

@ -75,8 +75,8 @@ let queued_root_effects = [];
let last_scheduled_effect = null;
let is_flushing = false;
let is_flushing_sync = false;
export class Batch {
/**
* The current values of any sources that are updated in this batch
@ -186,7 +186,7 @@ export class Batch {
// if there are multiple batches, we are 'time travelling' —
// we need to undo the changes belonging to any batch
// other than the current one
if (batches.size > 1) {
if (async_mode_flag && batches.size > 1) {
current_values = new Map();
batch_deriveds = new Map();
@ -483,6 +483,7 @@ export class Batch {
*/
export function flushSync(fn) {
if (async_mode_flag && active_effect !== null) {
// We disallow this because it creates super-hard to reason about stack trace and because it's generally a bad idea
e.flush_sync_in_effect();
}
@ -621,7 +622,9 @@ function flush_queued_effects(effects) {
}
}
if (eager_block_effects.length > 0) {
// If update_effect() has a flushSync() in it, we may have flushed another flush_queued_effects(),
// which already handled this logic and did set eager_block_effects to null.
if (eager_block_effects?.length > 0) {
// TODO this feels incorrect! it gets the tests passing
old_values.clear();
@ -677,6 +680,8 @@ export function suspend() {
if (!pending) {
batch.activate();
batch.decrement();
} else {
batch.deactivate();
}
unset_context();

@ -120,6 +120,9 @@ export function async_derived(fn, location) {
try {
var p = fn();
// Make sure to always access the then property to read any signals
// it might access, so that we track them as dependencies.
if (prev) Promise.resolve(p).catch(() => {}); // avoid unhandled rejection
} catch (error) {
p = Promise.reject(error);
}

@ -133,29 +133,40 @@ function create_effect(type, fn, sync, push = true) {
schedule_effect(effect);
}
// if an effect has no dependencies, no DOM and no teardown function,
// don't bother adding it to the effect tree
var inert =
sync &&
effect.deps === null &&
effect.first === null &&
effect.nodes_start === null &&
effect.teardown === null &&
(effect.f & EFFECT_PRESERVED) === 0;
if (!inert && push) {
if (parent !== null) {
push_effect(effect, parent);
}
if (push) {
/** @type {Effect | null} */
var e = effect;
// if we're in a derived, add the effect there too
// if an effect has already ran and doesn't need to be kept in the tree
// (because it won't re-run, has no DOM, and has no teardown etc)
// then we skip it and go to its child (if any)
if (
active_reaction !== null &&
(active_reaction.f & DERIVED) !== 0 &&
(type & ROOT_EFFECT) === 0
sync &&
e.deps === null &&
e.teardown === null &&
e.nodes_start === null &&
e.first === e.last && // either `null`, or a singular child
(e.f & EFFECT_PRESERVED) === 0
) {
var derived = /** @type {Derived} */ (active_reaction);
(derived.effects ??= []).push(effect);
e = e.first;
}
if (e !== null) {
e.parent = parent;
if (parent !== null) {
push_effect(e, parent);
}
// if we're in a derived, add the effect there too
if (
active_reaction !== null &&
(active_reaction.f & DERIVED) !== 0 &&
(type & ROOT_EFFECT) === 0
) {
var derived = /** @type {Derived} */ (active_reaction);
(derived.effects ??= []).push(e);
}
}
}
@ -242,7 +253,7 @@ export function inspect_effect(fn) {
*/
export function effect_root(fn) {
Batch.ensure();
const effect = create_effect(ROOT_EFFECT, fn, true);
const effect = create_effect(ROOT_EFFECT | EFFECT_PRESERVED, fn, true);
return () => {
destroy_effect(effect);
@ -256,7 +267,7 @@ export function effect_root(fn) {
*/
export function component_root(fn) {
Batch.ensure();
const effect = create_effect(ROOT_EFFECT, fn, true);
const effect = create_effect(ROOT_EFFECT | EFFECT_PRESERVED, fn, true);
return (options = {}) => {
return new Promise((fulfil) => {
@ -375,7 +386,7 @@ export function block(fn, flags = 0) {
* @param {boolean} [push]
*/
export function branch(fn, push = true) {
return create_effect(BRANCH_EFFECT, fn, true, push);
return create_effect(BRANCH_EFFECT | EFFECT_PRESERVED, fn, true, push);
}
/**

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

@ -0,0 +1,10 @@
import { test } from '../../test';
export default test({
async: true,
error: {
code: 'const_tag_invalid_reference',
message: 'The `{@const foo = ...}` declaration is not available in this snippet ',
position: [376, 379]
}
});

@ -0,0 +1,32 @@
<svelte:options runes />
<!-- ok -->
<svelte:boundary>
{@const foo = 'bar'}
{#snippet other()}
{foo}
{/snippet}
{foo}
<svelte:boundary>
{#snippet failed()}
{foo}
{/snippet}
</svelte:boundary>
{#snippet failed()}
{@const foo = 'bar'}
{foo}
{/snippet}
</svelte:boundary>
<!-- error -->
<svelte:boundary>
{@const foo = 'bar'}
{#snippet failed()}
{foo}
{/snippet}
</svelte:boundary>

@ -0,0 +1,10 @@
import { test } from '../../test';
export default test({
async: true,
error: {
code: 'const_tag_invalid_reference',
message: 'The `{@const foo = ...}` declaration is not available in this snippet ',
position: [298, 301]
}
});

@ -0,0 +1,27 @@
<svelte:options runes />
<!-- ok -->
<Component>
{@const foo = 'bar'}
{foo}
<Component>
{#snippet prop()}
{foo}
{/snippet}
</Component>
{#snippet prop()}
{@const foo = 'bar'}
{foo}
{/snippet}
</Component>
<!-- error -->
<Component>
{@const foo = 'bar'}
{#snippet prop()}
{foo}
{/snippet}
</Component>

@ -5,6 +5,7 @@ import { suite, type BaseTest } from '../suite';
import { read_file } from '../helpers.js';
interface CompilerErrorTest extends BaseTest {
async?: boolean;
error: {
code: string;
message: string;
@ -29,7 +30,8 @@ const { test, run } = suite<CompilerErrorTest>((config, cwd) => {
try {
compile(read_file(`${cwd}/main.svelte`), {
generate: 'client'
generate: 'client',
experimental: { async: config.async ?? false }
});
} catch (e) {
const error = e as CompileError;

@ -0,0 +1,15 @@
<script>
async function c(a) {
await Promise.resolve()
if (a) {
throw new Error('error');
} else {
return 'ok';
}
}
let a = $state();
let b = $derived(await c(a));
</script>
<button onclick={() => a = 1}>{b}</button>

@ -0,0 +1,19 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target }) {
await tick();
let [btn] = target.querySelectorAll('button');
btn.click();
await tick();
assert.htmlEqual(target.innerHTML, '<button>reset</button>');
[btn] = target.querySelectorAll('button');
btn.click();
await tick();
assert.htmlEqual(target.innerHTML, '<button>ok</button>');
}
});

@ -0,0 +1,12 @@
<script>
import Test from './Test.svelte';
</script>
<svelte:boundary>
<Test />
{#snippet pending()}pending{/snippet}
{#snippet failed(_, reset)}
<button onclick={reset}>reset</button>
{/snippet}
</svelte:boundary>

@ -7,6 +7,6 @@ export default test({
async test({ assert, target }) {
await tick();
assert.htmlEqual(target.innerHTML, `<h1>Hello, world!</h1>`);
assert.htmlEqual(target.innerHTML, `<h1>Hello, world!</h1> 5 01234`);
}
});

@ -3,6 +3,8 @@
</script>
<svelte:boundary>
{@const number = await Promise.resolve(5)}
{#snippet pending()}
<h1>Loading...</h1>
{/snippet}
@ -10,6 +12,14 @@
{#snippet greet()}
{@const greeting = await `Hello, ${name}!`}
<h1>{greeting}</h1>
{number}
{#if number > 4}
{@const length = await number}
{#each { length }, index}
{@const i = await index}
{i}
{/each}
{/if}
{/snippet}
{@render greet()}

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

@ -0,0 +1,35 @@
<script>
let count = $state(0);
let deferreds = [];
class X {
constructor(promise) {
this.promise = promise;
}
get then() {
count;
return (resolve) => this.promise.then(() => count).then(resolve)
}
}
function push() {
const deferred = Promise.withResolvers();
deferreds.push(deferred);
return new X(deferred.promise);
}
</script>
<button onclick={() => count += 1}>increment</button>
<button onclick={() => deferreds.pop()?.resolve(count)}>pop</button>
<svelte:boundary>
<p>{await push()}</p>
{#snippet pending()}
<p>loading...</p>
{/snippet}
</svelte:boundary>

@ -5,7 +5,14 @@ export default test({
async test({ assert, target }) {
const [increment, shift] = target.querySelectorAll('button');
assert.htmlEqual(target.innerHTML, `<p>loading...</p>`);
assert.htmlEqual(
target.innerHTML,
`
<button>increment</button>
<button>shift</button>
<p>loading...</p>
`
);
shift.click();
shift.click();

@ -18,13 +18,12 @@
<button onclick={() => shift()}>shift</button>
<svelte:boundary>
<svelte:boundary>
<p>{await push(value)}</p>
<p>{await push(value)}</p>
<p>{await push(value)}</p>
<p>inner pending: {$effect.pending()}</p>
</svelte:boundary>
<svelte:boundary>
<p>{await push(value)}</p>
<p>{await push(value)}</p>
<p>{await push(value)}</p>
<p>inner pending: {$effect.pending()}</p>
</svelte:boundary>
<p>outer pending: {$effect.pending()}</p>
{#snippet pending()}

@ -0,0 +1,7 @@
<script lang="ts">
import { resolve } from './main.svelte';
const bar = await new Promise((r) => resolve.push(() => r('bar')));
</script>
<p>bar: {bar}</p>

@ -0,0 +1,10 @@
<script lang="ts">
import { resolve } from './main.svelte';
import Bar from './Bar.svelte';
const foo = await new Promise((r) => resolve.push(() => r('foo')));
</script>
<p>foo: {foo}</p>
<Bar/>

@ -0,0 +1,42 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target }) {
const [show, resolve] = target.querySelectorAll('button');
show.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>show</button>
<button>resolve</button>
<p>pending...</p>
`
);
resolve.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>show</button>
<button>resolve</button>
<p>pending...</p>
`
);
resolve.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>show</button>
<button>resolve</button>
<p>foo: foo</p>
<p>bar: bar</p>
`
);
}
});

@ -0,0 +1,31 @@
<script module>
export let resolve = [];
</script>
<script>
import Foo from './Foo.svelte';
let show = $state(false);
</script>
<button onclick={() => show = true}>
show
</button>
<button onclick={() => resolve.shift()()}>
resolve
</button>
<svelte:boundary>
{#if show}
<Foo/>
{/if}
{#if $effect.pending()}
<p>pending...</p>
{/if}
{#snippet pending()}
<p>initializing...</p>
{/snippet}
</svelte:boundary>

@ -29,6 +29,7 @@ export default test({
<button>c</button>
<button>ok</button>
<p>c</p>
<p>b or c</p>
`
);
@ -46,6 +47,7 @@ export default test({
<button>c</button>
<button>ok</button>
<p>b</p>
<p>b or c</p>
`
);
}

@ -33,6 +33,10 @@
<p>c</p>
{/if}
{#if route === 'b' || route === 'c'}
<p>b or c</p>
{/if}
{#snippet pending()}
<p>pending...</p>
{/snippet}

@ -0,0 +1,9 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target }) {
await tick();
assert.htmlEqual(target.innerHTML, 'value');
}
});

@ -0,0 +1,9 @@
<script>
const value = await 'value';
</script>
{#snippet valueSnippet()}
{value}
{/snippet}
{@render valueSnippet()}

@ -0,0 +1,8 @@
<script>
import App from './app.svelte';
</script>
<svelte:boundary>
{#snippet pending()}
{/snippet}
<App />
</svelte:boundary>

@ -0,0 +1,9 @@
import { tick } from 'svelte';
import { ok, test } from '../../test';
export default test({
async test({ assert, target }) {
await tick();
assert.htmlEqual(target.innerHTML, '<p>foo bar</p>');
}
});

@ -0,0 +1,17 @@
<script>
function foo() {
return 'foo';
}
async function bar() {
return Promise.resolve('bar');
}
</script>
<svelte:boundary>
<p>{foo()} {await bar()}</p>
{#snippet pending()}
<p>pending</p>
{/snippet}
</svelte:boundary>

@ -0,0 +1,8 @@
<script lang="ts">
import { resolve } from './main.svelte';
const foo = $derived(await new Promise((r) => resolve.push(() => r('foo'))));
const bar = $derived(await new Promise((r) => resolve.push(() => r('bar'))));
</script>
<p>{foo} {bar}</p>

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

@ -0,0 +1,31 @@
<script module>
export let resolve = [];
</script>
<script>
import Foo from './Foo.svelte';
let show = $state(false);
</script>
<button onclick={() => show = true}>
show
</button>
<button onclick={() => resolve.shift()()}>
resolve
</button>
<svelte:boundary>
{#if show}
<Foo/>
{/if}
{#if $effect.pending()}
<p>pending...</p>
{/if}
{#snippet pending()}
<p>initializing...</p>
{/snippet}
</svelte:boundary>

@ -0,0 +1,28 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
mode: ['client', 'hydrate'],
html: `<input><p>a</a>`,
async test({ assert, target }) {
const [input] = target.querySelectorAll('input');
input.focus();
input.value = 'ab';
input.dispatchEvent(new InputEvent('input', { bubbles: true }));
flushSync();
assert.htmlEqual(target.innerHTML, `<input><p>ab</a>`);
assert.equal(input.value, 'ab');
input.focus();
input.value = 'abc';
input.dispatchEvent(new InputEvent('input', { bubbles: true }));
flushSync();
assert.htmlEqual(target.innerHTML, `<input><p>abc</a>`);
assert.equal(input.value, 'abc');
}
});

@ -0,0 +1,8 @@
<script>
let array = $state([{ value: 'a' }]);
</script>
{#each array as obj}
<input bind:value={() => obj.value, (value) => array = [{ value }]} />
<p>{obj.value}</p>
{/each}

@ -0,0 +1,30 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
mode: ['client', 'hydrate'],
async test({ assert, target }) {
const [input] = target.querySelectorAll('input');
input.focus();
input.value = 'Ab';
input.dispatchEvent(new InputEvent('input', { bubbles: true }));
await tick();
await tick();
assert.equal(input.value, 'AB');
assert.htmlEqual(target.innerHTML, `<input /><p>AB</p>`);
input.focus();
input.value = 'ABc';
input.dispatchEvent(new InputEvent('input', { bubbles: true }));
await tick();
await tick();
assert.equal(input.value, 'ABC');
assert.htmlEqual(target.innerHTML, `<input /><p>ABC</p>`);
}
});

@ -0,0 +1,6 @@
<script>
let text = $state('A');
</script>
<input bind:value={() => text, (v) => text = v.toUpperCase()} />
<p>{text}</p>

@ -0,0 +1,18 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
skip_async: true,
html: '<button></button><p>2</p>',
mode: ['client'],
test({ target, assert }) {
const btn = target.querySelector('button');
const p = target.querySelector('p');
flushSync(() => {
btn?.click();
});
assert.equal(p?.innerHTML, '4');
}
});

@ -0,0 +1,14 @@
<script>
import FlakyComponent from "./FlakyComponent.svelte";
let test=$state(1);
</script>
<button onclick={()=>test++}></button>
<svelte:boundary>
{@const double = test * 2}
{#snippet failed()}
<p>{double}</p>
{/snippet}
<FlakyComponent />
</svelte:boundary>

@ -2,7 +2,7 @@ import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
html: '<button></button><p>2</p>',
html: '<button>increment</button><p>2</p>',
mode: ['client'],
test({ target, assert }) {
const btn = target.querySelector('button');

@ -1,14 +1,10 @@
<script>
import FlakyComponent from "./FlakyComponent.svelte";
let test=$state(1);
let count = $state(1);
</script>
<button onclick={()=>test++}></button>
<button onclick={()=>count++}>increment</button>
<svelte:boundary>
{@const double = test * 2}
{#snippet failed()}
<p>{double}</p>
{/snippet}
<FlakyComponent />
</svelte:boundary>
{@const double = count * 2}
<p>{double}</p>
</svelte:boundary>

@ -20,5 +20,8 @@ export default test({
const [value1, value2] = target.querySelectorAll('value-element');
assert.equal(value1.shadowRoot?.innerHTML, '<span>test</span>');
assert.equal(value2.shadowRoot?.innerHTML, '<span>test</span>');
const value_builtin = target.querySelector('div');
assert.equal(value_builtin?.shadowRoot?.innerHTML, '<span>test</span>');
}
});

@ -15,6 +15,24 @@
}
});
}
if(!customElements.get('value-builtin')) {
customElements.define('value-builtin', class extends HTMLDivElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
}
set value(v) {
if (this.__value !== v) {
this.__value = v;
this.shadowRoot.innerHTML = `<span>${v}</span>`;
}
}
}, {
extends: 'div'
});
}
</script>
<my-element string="test" object={{ test: true }}></my-element>
@ -22,3 +40,4 @@
<value-element value="test"></value-element>
<value-element {...{value: "test"}}></value-element>
<div is="value-builtin" value="test"></div>

@ -0,0 +1,7 @@
<script>
let { text } = $props();
$effect(() => console.log(text));
</script>
{text}

@ -0,0 +1,12 @@
import { async_mode } from '../../../helpers';
import { test } from '../../test';
export default test({
// In legacy mode this succeeds and logs 'hello'
// In async mode this throws an error because flushSync is called inside an effect
async test({ assert, target, logs }) {
assert.htmlEqual(target.innerHTML, `<button>show</button> <div>hello</div>`);
assert.deepEqual(logs, ['hello']);
},
runtime_error: async_mode ? 'flush_sync_in_effect' : undefined
});

@ -0,0 +1,13 @@
<script>
import { flushSync, mount } from 'svelte'
import Child from './Child.svelte';
let show = $state(false);
</script>
<button onclick={() => show = true}>show</button>
<div {@attach (target) => {
mount(Child, { target, props: { text: 'hello' } });
flushSync();
}}></div>

@ -4,8 +4,6 @@
<svelte:boundary>
{@const x = a}
{#snippet failed()}
{x}
{/snippet}
{x}
<FlakyComponent />
</svelte:boundary>
</svelte:boundary>

Loading…
Cancel
Save