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

@ -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}` 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 ### constant_assignment
``` ```

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

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

@ -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}` 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_tag_invalid_arguments
> {@debug ...} arguments must be identifiers, not arbitrary expressions > {@debug ...} arguments must be identifiers, not arbitrary expressions

@ -2,7 +2,7 @@
"name": "svelte", "name": "svelte",
"description": "Cybernetically enhanced web apps", "description": "Cybernetically enhanced web apps",
"license": "MIT", "license": "MIT",
"version": "5.38.2", "version": "5.38.7",
"type": "module", "type": "module",
"types": "./types/index.d.ts", "types": "./types/index.d.ts",
"engines": { "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`); 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 * {@debug ...} arguments must be identifiers, not arbitrary expressions
* @param {null | number | NodeLike} node * @param {null | number | NodeLike} node

@ -7,6 +7,7 @@ import * as w from '../../../warnings.js';
import { is_rune } from '../../../../utils.js'; import { is_rune } from '../../../../utils.js';
import { mark_subtree_dynamic } from './shared/fragment.js'; import { mark_subtree_dynamic } from './shared/fragment.js';
import { get_rune } from '../../scope.js'; import { get_rune } from '../../scope.js';
import { is_component_node } from '../../nodes.js';
/** /**
* @param {Identifier} node * @param {Identifier} node
@ -155,5 +156,37 @@ export function Identifier(node, context) {
) { ) {
w.reactive_declaration_module_script_dependency(node); 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([ let component_block = b.block([
store_init, store_init,
...store_setup,
...legacy_reactive_declarations, ...legacy_reactive_declarations,
...group_binding_declarations, ...group_binding_declarations
...state.instance_level_snippets
]); ]);
const should_inject_context =
dev ||
analysis.needs_context ||
analysis.reactive_statements.size > 0 ||
component_returned_object.length > 0;
if (analysis.instance.has_await) { 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([ const body = b.block([
...store_setup,
...state.instance_level_snippets,
.../** @type {ESTree.Statement[]} */ (instance.body), .../** @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()), b.if(b.call('$.aborted'), b.return()),
.../** @type {ESTree.Statement[]} */ (template.body) .../** @type {ESTree.Statement[]} */ (template.body)
]); ]);
component_block.body.push(b.stmt(b.call(`$.async_body`, b.arrow([], body, true)))); component_block.body.push(b.stmt(b.call(`$.async_body`, b.arrow([], body, true))));
} else { } 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) { if (!analysis.runes && analysis.needs_context) {
component_block.body.push(b.stmt(b.call('$.init', analysis.immutable ? b.true : undefined))); 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 = let should_inject_props =
should_inject_context || should_inject_context ||
analysis.needs_props || analysis.needs_props ||
@ -442,7 +455,7 @@ export function client_component(analysis, options) {
let to_push; let to_push;
if (component_returned_object.length > 0) { 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); to_push = needs_store_cleanup ? b.var('$$pop', pop_call) : b.return(pop_call);
} else { } else {
to_push = b.stmt(b.call('$.pop')); to_push = b.stmt(b.call('$.pop'));
@ -453,6 +466,7 @@ export function client_component(analysis, options) {
if (needs_store_cleanup) { if (needs_store_cleanup) {
component_block.body.push(b.stmt(b.call('$$cleanup'))); component_block.body.push(b.stmt(b.call('$$cleanup')));
if (component_returned_object.length > 0) { if (component_returned_object.length > 0) {
component_block.body.push(b.return(b.id('$$pop'))); component_block.body.push(b.return(b.id('$$pop')));
} }

@ -8,7 +8,12 @@ import { dev, is_ignored } from '../../../../state.js';
* @param {ComponentContext} context * @param {ComponentContext} context
*/ */
export function ForOfStatement(node, 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 left = /** @type {VariableDeclaration | Pattern} */ (context.visit(node.left));
const argument = /** @type {Expression} */ (context.visit(node.right)); const argument = /** @type {Expression} */ (context.visit(node.right));
const body = /** @type {Statement} */ (context.visit(node.body)); 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 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 template_name = context.state.scope.root.unique('root'); // TODO infer name from parent
const unsuspend = b.id('$$unsuspend');
/** @type {Statement[]} */ /** @type {Statement[]} */
const body = []; 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); body.push(...state.consts);
if (has_await) { if (has_await) {
@ -182,8 +177,8 @@ export function Fragment(node, context) {
} }
if (has_await) { 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[]} */ /** @type {Statement[]} */
const hoisted = []; const hoisted = [];
let has_const = false;
// const tags need to live inside the boundary, but might also be referenced in hoisted snippets. // 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 // 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) { for (const child of node.fragment.nodes) {
if (child.type === 'ConstTag') { 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) { for (const child of node.fragment.nodes) {
if (child.type === 'ConstTag') { if (child.type === 'ConstTag') {
if (context.state.options.experimental.async) {
nodes.push(child);
}
continue; continue;
} }
if (child.type === 'SnippetBlock') { if (child.type === 'SnippetBlock') {
/** @type {Statement[]} */ if (
const statements = []; context.state.options.experimental.async &&
has_const &&
context.visit(child, { ...context.state, init: statements }); !['failed', 'pending'].includes(child.expression.name)
) {
const snippet = /** @type {VariableDeclaration} */ (statements[0]); // we can't hoist snippets as they may reference const tags, so we just keep them in the fragment
nodes.push(child);
const snippet_fn = dev } else {
? // @ts-expect-error we know this shape is correct /** @type {Statement[]} */
snippet.declarations[0].init.arguments[1] const statements = [];
: snippet.declarations[0].init;
context.visit(child, { ...context.state, init: statements });
snippet_fn.body.body.unshift(
...const_tags.filter((node) => node.type === 'VariableDeclaration') const snippet = /** @type {VariableDeclaration} */ (statements[0]);
);
const snippet_fn = dev
hoisted.push(snippet); ? // @ts-expect-error we know this shape is correct
snippet.declarations[0].init.arguments[1]
if (['failed', 'pending'].includes(child.expression.name)) { : snippet.declarations[0].init;
props.properties.push(b.prop('init', child.expression, child.expression));
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; continue;
@ -83,7 +103,9 @@ export function SvelteBoundary(node, context) {
const block = /** @type {BlockStatement} */ (context.visit({ ...node.fragment, nodes })); 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( const boundary = b.stmt(
b.call('$.boundary', context.state.node, props, b.arrow([b.id('$$anchor')], block)) b.call('$.boundary', context.state.node, props, b.arrow([b.id('$$anchor')], block))

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

@ -23,6 +23,15 @@ export function is_element_node(node) {
return element_nodes.includes(node.type); 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 * @param {AST.RegularElement | AST.SvelteElement} node
* @returns {boolean} * @returns {boolean}

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

@ -100,7 +100,7 @@ export function call(callee, ...args) {
if (typeof callee === 'string') callee = id(callee); if (typeof callee === 'string') callee = id(callee);
args = args.slice(); 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 i = args.length;
let popping = true; let popping = true;
while (i--) { while (i--) {
@ -108,7 +108,7 @@ export function call(callee, ...args) {
if (popping) { if (popping) {
args.pop(); args.pop();
} else { } else {
args[i] = id('undefined'); args[i] = void0;
} }
} else { } else {
popping = false; popping = false;

@ -393,6 +393,9 @@ export class Boundary {
e.svelte_boundary_reset_onerror(); 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 // this ensures we modify the cascading_pending_count of the correct parent
// by the number we're decreasing this boundary by // by the number we're decreasing this boundary by
this.update_pending_count(-this.#pending_count, true); 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, // Don't compute setters for custom elements while they aren't registered yet,
// because during their upgrade/instantiation they might add more setters. // because during their upgrade/instantiation they might add more setters.
// Instead, fall back to a simple "an object, then set as property" heuristic. // Instead, fall back to a simple "an object, then set as property" heuristic.
(setters_cache.has(node.nodeName) || (setters_cache.has(node.getAttribute('is') || node.nodeName) ||
// customElements may not be available in browser extension contexts // customElements may not be available in browser extension contexts
!customElements || !customElements ||
customElements.get(node.tagName.toLowerCase()) customElements.get(node.getAttribute('is') || node.tagName.toLowerCase())
? get_setters(node).includes(prop) ? get_setters(node).includes(prop)
: value && typeof value === 'object') : value && typeof value === 'object')
) { ) {
@ -546,9 +546,10 @@ var setters_cache = new Map();
/** @param {Element} element */ /** @param {Element} element */
function get_setters(element) { function get_setters(element) {
var setters = setters_cache.get(element.nodeName); var cache_key = element.getAttribute('is') || element.nodeName;
var setters = setters_cache.get(cache_key);
if (setters) return setters; if (setters) return setters;
setters_cache.set(element.nodeName, (setters = [])); setters_cache.set(cache_key, (setters = []));
var descriptors; var descriptors;
var proto = element; // In the case of custom elements there might be setters on the instance var proto = element; // In the case of custom elements there might be setters on the instance

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

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

@ -75,8 +75,8 @@ let queued_root_effects = [];
let last_scheduled_effect = null; let last_scheduled_effect = null;
let is_flushing = false; let is_flushing = false;
let is_flushing_sync = false; let is_flushing_sync = false;
export class Batch { export class Batch {
/** /**
* The current values of any sources that are updated in this 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' — // if there are multiple batches, we are 'time travelling' —
// we need to undo the changes belonging to any batch // we need to undo the changes belonging to any batch
// other than the current one // other than the current one
if (batches.size > 1) { if (async_mode_flag && batches.size > 1) {
current_values = new Map(); current_values = new Map();
batch_deriveds = new Map(); batch_deriveds = new Map();
@ -483,6 +483,7 @@ export class Batch {
*/ */
export function flushSync(fn) { export function flushSync(fn) {
if (async_mode_flag && active_effect !== null) { if (async_mode_flag && active_effect !== null) {
// We disallow this because it creates super-hard to reason about stack trace and because it's generally a bad idea
e.flush_sync_in_effect(); e.flush_sync_in_effect();
} }
@ -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 // TODO this feels incorrect! it gets the tests passing
old_values.clear(); old_values.clear();
@ -677,6 +680,8 @@ export function suspend() {
if (!pending) { if (!pending) {
batch.activate(); batch.activate();
batch.decrement(); batch.decrement();
} else {
batch.deactivate();
} }
unset_context(); unset_context();

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

@ -133,29 +133,40 @@ function create_effect(type, fn, sync, push = true) {
schedule_effect(effect); schedule_effect(effect);
} }
// if an effect has no dependencies, no DOM and no teardown function, if (push) {
// don't bother adding it to the effect tree /** @type {Effect | null} */
var inert = var e = effect;
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 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 ( if (
active_reaction !== null && sync &&
(active_reaction.f & DERIVED) !== 0 && e.deps === null &&
(type & ROOT_EFFECT) === 0 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); e = e.first;
(derived.effects ??= []).push(effect); }
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) { export function effect_root(fn) {
Batch.ensure(); Batch.ensure();
const effect = create_effect(ROOT_EFFECT, fn, true); const effect = create_effect(ROOT_EFFECT | EFFECT_PRESERVED, fn, true);
return () => { return () => {
destroy_effect(effect); destroy_effect(effect);
@ -256,7 +267,7 @@ export function effect_root(fn) {
*/ */
export function component_root(fn) { export function component_root(fn) {
Batch.ensure(); Batch.ensure();
const effect = create_effect(ROOT_EFFECT, fn, true); const effect = create_effect(ROOT_EFFECT | EFFECT_PRESERVED, fn, true);
return (options = {}) => { return (options = {}) => {
return new Promise((fulfil) => { return new Promise((fulfil) => {
@ -375,7 +386,7 @@ export function block(fn, flags = 0) {
* @param {boolean} [push] * @param {boolean} [push]
*/ */
export function branch(fn, push = true) { 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. * The current version, as set in package.json.
* @type {string} * @type {string}
*/ */
export const VERSION = '5.38.2'; export const VERSION = '5.38.7';
export const PUBLIC_VERSION = '5'; 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'; import { read_file } from '../helpers.js';
interface CompilerErrorTest extends BaseTest { interface CompilerErrorTest extends BaseTest {
async?: boolean;
error: { error: {
code: string; code: string;
message: string; message: string;
@ -29,7 +30,8 @@ const { test, run } = suite<CompilerErrorTest>((config, cwd) => {
try { try {
compile(read_file(`${cwd}/main.svelte`), { compile(read_file(`${cwd}/main.svelte`), {
generate: 'client' generate: 'client',
experimental: { async: config.async ?? false }
}); });
} catch (e) { } catch (e) {
const error = e as CompileError; 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 }) { async test({ assert, target }) {
await tick(); 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> </script>
<svelte:boundary> <svelte:boundary>
{@const number = await Promise.resolve(5)}
{#snippet pending()} {#snippet pending()}
<h1>Loading...</h1> <h1>Loading...</h1>
{/snippet} {/snippet}
@ -10,6 +12,14 @@
{#snippet greet()} {#snippet greet()}
{@const greeting = await `Hello, ${name}!`} {@const greeting = await `Hello, ${name}!`}
<h1>{greeting}</h1> <h1>{greeting}</h1>
{number}
{#if number > 4}
{@const length = await number}
{#each { length }, index}
{@const i = await index}
{i}
{/each}
{/if}
{/snippet} {/snippet}
{@render greet()} {@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 }) { async test({ assert, target }) {
const [increment, shift] = target.querySelectorAll('button'); 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();
shift.click(); shift.click();

@ -18,13 +18,12 @@
<button onclick={() => shift()}>shift</button> <button onclick={() => shift()}>shift</button>
<svelte:boundary> <svelte:boundary>
<svelte:boundary>
<svelte:boundary> <p>{await push(value)}</p>
<p>{await push(value)}</p> <p>{await push(value)}</p>
<p>{await push(value)}</p> <p>{await push(value)}</p>
<p>{await push(value)}</p> <p>inner pending: {$effect.pending()}</p>
<p>inner pending: {$effect.pending()}</p> </svelte:boundary>
</svelte:boundary>
<p>outer pending: {$effect.pending()}</p> <p>outer pending: {$effect.pending()}</p>
{#snippet pending()} {#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>c</button>
<button>ok</button> <button>ok</button>
<p>c</p> <p>c</p>
<p>b or c</p>
` `
); );
@ -46,6 +47,7 @@ export default test({
<button>c</button> <button>c</button>
<button>ok</button> <button>ok</button>
<p>b</p> <p>b</p>
<p>b or c</p>
` `
); );
} }

@ -33,6 +33,10 @@
<p>c</p> <p>c</p>
{/if} {/if}
{#if route === 'b' || route === 'c'}
<p>b or c</p>
{/if}
{#snippet pending()} {#snippet pending()}
<p>pending...</p> <p>pending...</p>
{/snippet} {/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'; import { test } from '../../test';
export default test({ export default test({
html: '<button></button><p>2</p>', html: '<button>increment</button><p>2</p>',
mode: ['client'], mode: ['client'],
test({ target, assert }) { test({ target, assert }) {
const btn = target.querySelector('button'); const btn = target.querySelector('button');

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

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

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

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

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

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

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

Loading…
Cancel
Save