diff --git a/.changeset/dirty-cycles-smash.md b/.changeset/dirty-cycles-smash.md new file mode 100644 index 0000000000..1b031cf0af --- /dev/null +++ b/.changeset/dirty-cycles-smash.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: send `$effect.pending` count to the correct boundary diff --git a/.changeset/silent-suns-whisper.md b/.changeset/silent-suns-whisper.md deleted file mode 100644 index 7ee7d74abc..0000000000 --- a/.changeset/silent-suns-whisper.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: properly catch top level await errors diff --git a/documentation/docs/07-misc/02-testing.md b/documentation/docs/07-misc/02-testing.md index bcec4db0a3..23e2e023d3 100644 --- a/documentation/docs/07-misc/02-testing.md +++ b/documentation/docs/07-misc/02-testing.md @@ -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 + + + + + { + // 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('You’re 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/). diff --git a/documentation/docs/07-misc/04-custom-elements.md b/documentation/docs/07-misc/04-custom-elements.md index 7e6a17b947..4e5afff7d2 100644 --- a/documentation/docs/07-misc/04-custom-elements.md +++ b/documentation/docs/07-misc/04-custom-elements.md @@ -4,7 +4,7 @@ title: Custom elements -Svelte components can also be compiled to custom elements (aka web components) using the `customElement: true` compiler option. You should specify a tag name for the component using the `` [element](svelte-options). +Svelte components can also be compiled to custom elements (aka web components) using the `customElement: true` compiler option. You should specify a tag name for the component using the `` [element](svelte-options). Within the custom element you can access the host element via the [`$host`](https://svelte.dev/docs/svelte/$host) rune. ```svelte diff --git a/documentation/docs/98-reference/.generated/compile-errors.md b/documentation/docs/98-reference/.generated/compile-errors.md index 957a9f67c7..b9c44163c9 100644 --- a/documentation/docs/98-reference/.generated/compile-errors.md +++ b/documentation/docs/98-reference/.generated/compile-errors.md @@ -196,6 +196,51 @@ Cyclical dependency detected: %cycle% `{@const}` must be the immediate child of `{#snippet}`, `{#if}`, `{:else if}`, `{:else}`, `{#each}`, `{:then}`, `{:catch}`, ``, `` ``` +### const_tag_invalid_reference + +``` +The `{@const %name% = ...}` declaration is not available in this snippet +``` + +The following is an error: + +```svelte + + {@const foo = 'bar'} + + {#snippet failed()} + {foo} + {/snippet} + +``` + +Here, `foo` is not available inside `failed`. The top level code inside `` becomes part of the implicit `children` snippet, in other words the above code is equivalent to this: + +```svelte + + {#snippet children()} + {@const foo = 'bar'} + {/snippet} + + {#snippet failed()} + {foo} + {/snippet} + +``` + +The same applies to components: + +```svelte + + {@const foo = 'bar'} + + {#snippet someProp()} + + {foo} + {/snippet} + +``` + ### constant_assignment ``` diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index cd6a4a916c..535214781c 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -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 diff --git a/packages/svelte/elements.d.ts b/packages/svelte/elements.d.ts index f63a31a96b..b0c2fae2de 100644 --- a/packages/svelte/elements.d.ts +++ b/packages/svelte/elements.d.ts @@ -1268,6 +1268,7 @@ export interface HTMLMetaAttributes extends HTMLAttributes { charset?: string | undefined | null; content?: string | undefined | null; 'http-equiv'?: + | 'accept-ch' | 'content-security-policy' | 'content-type' | 'default-style' diff --git a/packages/svelte/messages/compile-errors/template.md b/packages/svelte/messages/compile-errors/template.md index 0569f63ad3..dc26a02767 100644 --- a/packages/svelte/messages/compile-errors/template.md +++ b/packages/svelte/messages/compile-errors/template.md @@ -124,6 +124,49 @@ > `{@const}` must be the immediate child of `{#snippet}`, `{#if}`, `{:else if}`, `{:else}`, `{#each}`, `{:then}`, `{:catch}`, ``, `` +## const_tag_invalid_reference + +> The `{@const %name% = ...}` declaration is not available in this snippet + +The following is an error: + +```svelte + + {@const foo = 'bar'} + + {#snippet failed()} + {foo} + {/snippet} + +``` + +Here, `foo` is not available inside `failed`. The top level code inside `` becomes part of the implicit `children` snippet, in other words the above code is equivalent to this: + +```svelte + + {#snippet children()} + {@const foo = 'bar'} + {/snippet} + + {#snippet failed()} + {foo} + {/snippet} + +``` + +The same applies to components: + +```svelte + + {@const foo = 'bar'} + + {#snippet someProp()} + + {foo} + {/snippet} + +``` + ## debug_tag_invalid_arguments > {@debug ...} arguments must be identifiers, not arbitrary expressions diff --git a/packages/svelte/package.json b/packages/svelte/package.json index fc7db9598d..c6bc40ae2c 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -2,7 +2,7 @@ "name": "svelte", "description": "Cybernetically enhanced web apps", "license": "MIT", - "version": "5.38.2", + "version": "5.38.7", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/compiler/errors.js b/packages/svelte/src/compiler/errors.js index e763a6e073..44fc641ee5 100644 --- a/packages/svelte/src/compiler/errors.js +++ b/packages/svelte/src/compiler/errors.js @@ -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}\`, \`\`, \`\`\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 diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/Identifier.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/Identifier.js index 4dfdfe5af1..1c98a95e63 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/Identifier.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/Identifier.js @@ -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; + } + } + } + } } } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js index 940d6a9e00..706d2b4e10 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js @@ -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'))); } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ForOfStatement.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ForOfStatement.js index a5d2751812..8ae67f49d1 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ForOfStatement.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ForOfStatement.js @@ -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)); diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Fragment.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Fragment.js index c7c576101e..85d8e3caff 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Fragment.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Fragment.js @@ -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); } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteBoundary.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteBoundary.js index 70df022355..49c89bc438 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteBoundary.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteBoundary.js @@ -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)) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js index 014547cf2d..ba140a153e 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js @@ -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; }); diff --git a/packages/svelte/src/compiler/phases/nodes.js b/packages/svelte/src/compiler/phases/nodes.js index 4874554ff0..f4127db359 100644 --- a/packages/svelte/src/compiler/phases/nodes.js +++ b/packages/svelte/src/compiler/phases/nodes.js @@ -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} diff --git a/packages/svelte/src/compiler/phases/scope.js b/packages/svelte/src/compiler/phases/scope.js index f88f5ef8b1..76157d406f 100644 --- a/packages/svelte/src/compiler/phases/scope.js +++ b/packages/svelte/src/compiler/phases/scope.js @@ -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); } } diff --git a/packages/svelte/src/compiler/utils/builders.js b/packages/svelte/src/compiler/utils/builders.js index 56a5f31ffe..03a946ff9c 100644 --- a/packages/svelte/src/compiler/utils/builders.js +++ b/packages/svelte/src/compiler/utils/builders.js @@ -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; diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index ab42ded5bc..1f78b4cf11 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -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); diff --git a/packages/svelte/src/internal/client/dom/elements/attributes.js b/packages/svelte/src/internal/client/dom/elements/attributes.js index 2fa5d4541c..a5b7140f25 100644 --- a/packages/svelte/src/internal/client/dom/elements/attributes.js +++ b/packages/svelte/src/internal/client/dom/elements/attributes.js @@ -238,10 +238,10 @@ export function set_custom_element_data(node, prop, value) { // Don't compute setters for custom elements while they aren't registered yet, // because during their upgrade/instantiation they might add more setters. // Instead, fall back to a simple "an object, then set as property" heuristic. - (setters_cache.has(node.nodeName) || + (setters_cache.has(node.getAttribute('is') || node.nodeName) || // customElements may not be available in browser extension contexts !customElements || - customElements.get(node.tagName.toLowerCase()) + customElements.get(node.getAttribute('is') || node.tagName.toLowerCase()) ? get_setters(node).includes(prop) : value && typeof value === 'object') ) { @@ -546,9 +546,10 @@ var setters_cache = new Map(); /** @param {Element} element */ function get_setters(element) { - var setters = setters_cache.get(element.nodeName); + var cache_key = element.getAttribute('is') || element.nodeName; + var setters = setters_cache.get(cache_key); if (setters) return setters; - setters_cache.set(element.nodeName, (setters = [])); + setters_cache.set(cache_key, (setters = [])); var descriptors; var proto = element; // In the case of custom elements there might be setters on the instance diff --git a/packages/svelte/src/internal/client/dom/elements/bindings/input.js b/packages/svelte/src/internal/client/dom/elements/bindings/input.js index 67e6ff1dd2..815acde7c5 100644 --- a/packages/svelte/src/internal/client/dom/elements/bindings/input.js +++ b/packages/svelte/src/internal/client/dom/elements/bindings/input.js @@ -6,7 +6,7 @@ import * as e from '../../../errors.js'; import { is } from '../../../proxy.js'; import { queue_micro_task } from '../../task.js'; import { hydrating } from '../../hydration.js'; -import { untrack } from '../../../runtime.js'; +import { tick, untrack } from '../../../runtime.js'; import { is_runes } from '../../../context.js'; import { current_batch, previous_batch } from '../../../reactivity/batch.js'; @@ -17,11 +17,9 @@ import { current_batch, previous_batch } from '../../../reactivity/batch.js'; * @returns {void} */ export function bind_value(input, get, set = get) { - var runes = is_runes(); - var batches = new WeakSet(); - listen_to_event_and_reset_event(input, 'input', (is_reset) => { + listen_to_event_and_reset_event(input, 'input', async (is_reset) => { if (DEV && input.type === 'checkbox') { // TODO should this happen in prod too? e.bind_invalid_checkbox_value(); @@ -36,9 +34,13 @@ export function bind_value(input, get, set = get) { batches.add(current_batch); } - // In runes mode, respect any validation in accessors (doesn't apply in legacy mode, - // because we use mutable state which ensures the render effect always runs) - if (runes && value !== (value = get())) { + // Because `{#each ...}` blocks work by updating sources inside the flush, + // we need to wait a tick before checking to see if we should forcibly + // update the input and reset the selection state + await tick(); + + // Respect any validation in accessors + if (value !== (value = get())) { var start = input.selectionStart; var end = input.selectionEnd; diff --git a/packages/svelte/src/internal/client/reactivity/async.js b/packages/svelte/src/internal/client/reactivity/async.js index cd8eff3ffc..f91f2e094a 100644 --- a/packages/svelte/src/internal/client/reactivity/async.js +++ b/packages/svelte/src/internal/client/reactivity/async.js @@ -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} 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(); diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index f51cad8e98..fd5c34ab26 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -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(); diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index 7f730e365e..31dc267960 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -120,6 +120,9 @@ export function async_derived(fn, location) { try { var p = fn(); + // Make sure to always access the then property to read any signals + // it might access, so that we track them as dependencies. + if (prev) Promise.resolve(p).catch(() => {}); // avoid unhandled rejection } catch (error) { p = Promise.reject(error); } diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index df3dd75808..2c9e4db911 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -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); } /** diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index 2aa62504a5..d499c06797 100644 --- a/packages/svelte/src/version.js +++ b/packages/svelte/src/version.js @@ -4,5 +4,5 @@ * The current version, as set in package.json. * @type {string} */ -export const VERSION = '5.38.2'; +export const VERSION = '5.38.7'; export const PUBLIC_VERSION = '5'; diff --git a/packages/svelte/tests/compiler-errors/samples/const-tag-snippet-invalid-reference-1/_config.js b/packages/svelte/tests/compiler-errors/samples/const-tag-snippet-invalid-reference-1/_config.js new file mode 100644 index 0000000000..7424278180 --- /dev/null +++ b/packages/svelte/tests/compiler-errors/samples/const-tag-snippet-invalid-reference-1/_config.js @@ -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] + } +}); diff --git a/packages/svelte/tests/compiler-errors/samples/const-tag-snippet-invalid-reference-1/main.svelte b/packages/svelte/tests/compiler-errors/samples/const-tag-snippet-invalid-reference-1/main.svelte new file mode 100644 index 0000000000..a2533e33b0 --- /dev/null +++ b/packages/svelte/tests/compiler-errors/samples/const-tag-snippet-invalid-reference-1/main.svelte @@ -0,0 +1,32 @@ + + + + + {@const foo = 'bar'} + + {#snippet other()} + {foo} + {/snippet} + + {foo} + + + {#snippet failed()} + {foo} + {/snippet} + + + {#snippet failed()} + {@const foo = 'bar'} + {foo} + {/snippet} + + + + + {@const foo = 'bar'} + + {#snippet failed()} + {foo} + {/snippet} + diff --git a/packages/svelte/tests/compiler-errors/samples/const-tag-snippet-invalid-reference-2/_config.js b/packages/svelte/tests/compiler-errors/samples/const-tag-snippet-invalid-reference-2/_config.js new file mode 100644 index 0000000000..7ff71a61f9 --- /dev/null +++ b/packages/svelte/tests/compiler-errors/samples/const-tag-snippet-invalid-reference-2/_config.js @@ -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] + } +}); diff --git a/packages/svelte/tests/compiler-errors/samples/const-tag-snippet-invalid-reference-2/main.svelte b/packages/svelte/tests/compiler-errors/samples/const-tag-snippet-invalid-reference-2/main.svelte new file mode 100644 index 0000000000..c59df28ec9 --- /dev/null +++ b/packages/svelte/tests/compiler-errors/samples/const-tag-snippet-invalid-reference-2/main.svelte @@ -0,0 +1,27 @@ + + + + + {@const foo = 'bar'} + {foo} + + + {#snippet prop()} + {foo} + {/snippet} + + + {#snippet prop()} + {@const foo = 'bar'} + {foo} + {/snippet} + + + + + {@const foo = 'bar'} + + {#snippet prop()} + {foo} + {/snippet} + diff --git a/packages/svelte/tests/compiler-errors/test.ts b/packages/svelte/tests/compiler-errors/test.ts index 13b9280dde..b3a2d4af31 100644 --- a/packages/svelte/tests/compiler-errors/test.ts +++ b/packages/svelte/tests/compiler-errors/test.ts @@ -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((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; diff --git a/packages/svelte/tests/runtime-runes/samples/async-boundary-reset/Test.svelte b/packages/svelte/tests/runtime-runes/samples/async-boundary-reset/Test.svelte new file mode 100644 index 0000000000..2232a094cb --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-boundary-reset/Test.svelte @@ -0,0 +1,15 @@ + + + diff --git a/packages/svelte/tests/runtime-runes/samples/async-boundary-reset/_config.js b/packages/svelte/tests/runtime-runes/samples/async-boundary-reset/_config.js new file mode 100644 index 0000000000..19b273175b --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-boundary-reset/_config.js @@ -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, ''); + + [btn] = target.querySelectorAll('button'); + btn.click(); + await tick(); + + assert.htmlEqual(target.innerHTML, ''); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-boundary-reset/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-boundary-reset/main.svelte new file mode 100644 index 0000000000..42242f26d6 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-boundary-reset/main.svelte @@ -0,0 +1,12 @@ + + + + + + {#snippet pending()}pending{/snippet} + {#snippet failed(_, reset)} + + {/snippet} + diff --git a/packages/svelte/tests/runtime-runes/samples/async-const/_config.js b/packages/svelte/tests/runtime-runes/samples/async-const/_config.js index 084d9c3874..8aeca875f3 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-const/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-const/_config.js @@ -7,6 +7,6 @@ export default test({ async test({ assert, target }) { await tick(); - assert.htmlEqual(target.innerHTML, `

Hello, world!

`); + assert.htmlEqual(target.innerHTML, `

Hello, world!

5 01234`); } }); diff --git a/packages/svelte/tests/runtime-runes/samples/async-const/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-const/main.svelte index 9321bd7929..7410ff6a6f 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-const/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/async-const/main.svelte @@ -3,6 +3,8 @@ + {@const number = await Promise.resolve(5)} + {#snippet pending()}

Loading...

{/snippet} @@ -10,6 +12,14 @@ {#snippet greet()} {@const greeting = await `Hello, ${name}!`}

{greeting}

+ {number} + {#if number > 4} + {@const length = await number} + {#each { length }, index} + {@const i = await index} + {i} + {/each} + {/if} {/snippet} {@render greet()} diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-reverse-order/_config.js b/packages/svelte/tests/runtime-runes/samples/async-derived-reverse-order/_config.js new file mode 100644 index 0000000000..bd0dd753c2 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-derived-reverse-order/_config.js @@ -0,0 +1,41 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + const [increment, pop] = target.querySelectorAll('button'); + + increment.click(); + await tick(); + + pop.click(); + await tick(); + + pop.click(); + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` + + +

1

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

2

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

{await push()}

+ + {#snippet pending()} +

loading...

+ {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-effect-pending-nested/_config.js b/packages/svelte/tests/runtime-runes/samples/async-effect-pending-nested/_config.js index a2cad1bb4e..9fe354bac0 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-effect-pending-nested/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-effect-pending-nested/_config.js @@ -5,7 +5,14 @@ export default test({ async test({ assert, target }) { const [increment, shift] = target.querySelectorAll('button'); - assert.htmlEqual(target.innerHTML, `

loading...

`); + assert.htmlEqual( + target.innerHTML, + ` + + +

loading...

+ ` + ); shift.click(); shift.click(); diff --git a/packages/svelte/tests/runtime-runes/samples/async-effect-pending-nested/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-effect-pending-nested/main.svelte index bf98bfb4bd..eeafbdc3c4 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-effect-pending-nested/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/async-effect-pending-nested/main.svelte @@ -18,13 +18,12 @@ - - -

{await push(value)}

-

{await push(value)}

-

{await push(value)}

-

inner pending: {$effect.pending()}

-
+ +

{await push(value)}

+

{await push(value)}

+

{await push(value)}

+

inner pending: {$effect.pending()}

+

outer pending: {$effect.pending()}

{#snippet pending()} diff --git a/packages/svelte/tests/runtime-runes/samples/async-nested-top-level/Bar.svelte b/packages/svelte/tests/runtime-runes/samples/async-nested-top-level/Bar.svelte new file mode 100644 index 0000000000..f1ac9ab760 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-nested-top-level/Bar.svelte @@ -0,0 +1,7 @@ + + +

bar: {bar}

diff --git a/packages/svelte/tests/runtime-runes/samples/async-nested-top-level/Foo.svelte b/packages/svelte/tests/runtime-runes/samples/async-nested-top-level/Foo.svelte new file mode 100644 index 0000000000..e2029a3033 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-nested-top-level/Foo.svelte @@ -0,0 +1,10 @@ + + +

foo: {foo}

+ + diff --git a/packages/svelte/tests/runtime-runes/samples/async-nested-top-level/_config.js b/packages/svelte/tests/runtime-runes/samples/async-nested-top-level/_config.js new file mode 100644 index 0000000000..ca7965bf79 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-nested-top-level/_config.js @@ -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, + ` + + +

pending...

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

pending...

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

foo: foo

+

bar: bar

+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-nested-top-level/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-nested-top-level/main.svelte new file mode 100644 index 0000000000..bd0efaa4f8 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-nested-top-level/main.svelte @@ -0,0 +1,31 @@ + + + + + + + + + + {#if show} + + {/if} + + {#if $effect.pending()} +

pending...

+ {/if} + + {#snippet pending()} +

initializing...

+ {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-redirect/_config.js b/packages/svelte/tests/runtime-runes/samples/async-redirect/_config.js index ebbe642860..fe92977c21 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-redirect/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-redirect/_config.js @@ -29,6 +29,7 @@ export default test({

c

+

b or c

` ); @@ -46,6 +47,7 @@ export default test({

b

+

b or c

` ); } diff --git a/packages/svelte/tests/runtime-runes/samples/async-redirect/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-redirect/main.svelte index bf5fdf9ed3..aead1b00e5 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-redirect/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/async-redirect/main.svelte @@ -33,6 +33,10 @@

c

{/if} + {#if route === 'b' || route === 'c'} +

b or c

+ {/if} + {#snippet pending()}

pending...

{/snippet} diff --git a/packages/svelte/tests/runtime-runes/samples/async-reference-in-snippet/_config.js b/packages/svelte/tests/runtime-runes/samples/async-reference-in-snippet/_config.js new file mode 100644 index 0000000000..c6903c3eed --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-reference-in-snippet/_config.js @@ -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'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-reference-in-snippet/app.svelte b/packages/svelte/tests/runtime-runes/samples/async-reference-in-snippet/app.svelte new file mode 100644 index 0000000000..27b29cfe50 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-reference-in-snippet/app.svelte @@ -0,0 +1,9 @@ + + +{#snippet valueSnippet()} + {value} +{/snippet} + +{@render valueSnippet()} \ No newline at end of file diff --git a/packages/svelte/tests/runtime-runes/samples/async-reference-in-snippet/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-reference-in-snippet/main.svelte new file mode 100644 index 0000000000..c251a5645b --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-reference-in-snippet/main.svelte @@ -0,0 +1,8 @@ + + + {#snippet pending()} + {/snippet} + + \ No newline at end of file diff --git a/packages/svelte/tests/runtime-runes/samples/async-template-async-sync-mixed/_config.js b/packages/svelte/tests/runtime-runes/samples/async-template-async-sync-mixed/_config.js new file mode 100644 index 0000000000..709b88578f --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-template-async-sync-mixed/_config.js @@ -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, '

foo bar

'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-template-async-sync-mixed/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-template-async-sync-mixed/main.svelte new file mode 100644 index 0000000000..2e0ae46f1f --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-template-async-sync-mixed/main.svelte @@ -0,0 +1,17 @@ + + + +

{foo()} {await bar()}

+ + {#snippet pending()} +

pending

+ {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-top-level-deriveds/Foo.svelte b/packages/svelte/tests/runtime-runes/samples/async-top-level-deriveds/Foo.svelte new file mode 100644 index 0000000000..e8a7c84137 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-top-level-deriveds/Foo.svelte @@ -0,0 +1,8 @@ + + +

{foo} {bar}

diff --git a/packages/svelte/tests/runtime-runes/samples/async-top-level-deriveds/_config.js b/packages/svelte/tests/runtime-runes/samples/async-top-level-deriveds/_config.js new file mode 100644 index 0000000000..2c7ffd3952 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-top-level-deriveds/_config.js @@ -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, + ` + + +

pending...

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

pending...

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

foo bar

+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-top-level-deriveds/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-top-level-deriveds/main.svelte new file mode 100644 index 0000000000..bd0efaa4f8 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-top-level-deriveds/main.svelte @@ -0,0 +1,31 @@ + + + + + + + + + + {#if show} + + {/if} + + {#if $effect.pending()} +

pending...

+ {/if} + + {#snippet pending()} +

initializing...

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

a`, + + async test({ assert, target }) { + const [input] = target.querySelectorAll('input'); + + input.focus(); + input.value = 'ab'; + input.dispatchEvent(new InputEvent('input', { bubbles: true })); + flushSync(); + + assert.htmlEqual(target.innerHTML, `

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

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

{obj.value}

+{/each} diff --git a/packages/svelte/tests/runtime-runes/samples/binding-update-while-focused-3/_config.js b/packages/svelte/tests/runtime-runes/samples/binding-update-while-focused-3/_config.js new file mode 100644 index 0000000000..0909dee7a8 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/binding-update-while-focused-3/_config.js @@ -0,0 +1,30 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + mode: ['client', 'hydrate'], + + async test({ assert, target }) { + const [input] = target.querySelectorAll('input'); + + input.focus(); + input.value = 'Ab'; + input.dispatchEvent(new InputEvent('input', { bubbles: true })); + + await tick(); + await tick(); + + assert.equal(input.value, 'AB'); + assert.htmlEqual(target.innerHTML, `

AB

`); + + input.focus(); + input.value = 'ABc'; + input.dispatchEvent(new InputEvent('input', { bubbles: true })); + + await tick(); + await tick(); + + assert.equal(input.value, 'ABC'); + assert.htmlEqual(target.innerHTML, `

ABC

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

{text}

diff --git a/packages/svelte/tests/runtime-runes/samples/const-tag-boundary/FlakyComponent.svelte b/packages/svelte/tests/runtime-runes/samples/const-tag-boundary-deprecated-usage/FlakyComponent.svelte similarity index 74% rename from packages/svelte/tests/runtime-runes/samples/const-tag-boundary/FlakyComponent.svelte rename to packages/svelte/tests/runtime-runes/samples/const-tag-boundary-deprecated-usage/FlakyComponent.svelte index 8bbec90de4..ea60542af9 100644 --- a/packages/svelte/tests/runtime-runes/samples/const-tag-boundary/FlakyComponent.svelte +++ b/packages/svelte/tests/runtime-runes/samples/const-tag-boundary-deprecated-usage/FlakyComponent.svelte @@ -1,3 +1,3 @@ \ No newline at end of file + diff --git a/packages/svelte/tests/runtime-runes/samples/const-tag-boundary-deprecated-usage/_config.js b/packages/svelte/tests/runtime-runes/samples/const-tag-boundary-deprecated-usage/_config.js new file mode 100644 index 0000000000..915bda91f3 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/const-tag-boundary-deprecated-usage/_config.js @@ -0,0 +1,18 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + skip_async: true, + html: '

2

', + mode: ['client'], + test({ target, assert }) { + const btn = target.querySelector('button'); + const p = target.querySelector('p'); + + flushSync(() => { + btn?.click(); + }); + + assert.equal(p?.innerHTML, '4'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/const-tag-boundary-deprecated-usage/main.svelte b/packages/svelte/tests/runtime-runes/samples/const-tag-boundary-deprecated-usage/main.svelte new file mode 100644 index 0000000000..25ea8a3ffc --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/const-tag-boundary-deprecated-usage/main.svelte @@ -0,0 +1,14 @@ + + + + + + {@const double = test * 2} + {#snippet failed()} +

{double}

+ {/snippet} + +
\ No newline at end of file diff --git a/packages/svelte/tests/runtime-runes/samples/const-tag-boundary/_config.js b/packages/svelte/tests/runtime-runes/samples/const-tag-boundary/_config.js index 4338969a48..e4ffb4a850 100644 --- a/packages/svelte/tests/runtime-runes/samples/const-tag-boundary/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/const-tag-boundary/_config.js @@ -2,7 +2,7 @@ import { flushSync } from 'svelte'; import { test } from '../../test'; export default test({ - html: '

2

', + html: '

2

', mode: ['client'], test({ target, assert }) { const btn = target.querySelector('button'); diff --git a/packages/svelte/tests/runtime-runes/samples/const-tag-boundary/main.svelte b/packages/svelte/tests/runtime-runes/samples/const-tag-boundary/main.svelte index 25ea8a3ffc..9605e12070 100644 --- a/packages/svelte/tests/runtime-runes/samples/const-tag-boundary/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/const-tag-boundary/main.svelte @@ -1,14 +1,10 @@ - + - {@const double = test * 2} - {#snippet failed()} -

{double}

- {/snippet} - -
\ No newline at end of file + {@const double = count * 2} +

{double}

+
diff --git a/packages/svelte/tests/runtime-runes/samples/custom-element-attributes/_config.js b/packages/svelte/tests/runtime-runes/samples/custom-element-attributes/_config.js index 7f406d8f0d..3d8917c147 100644 --- a/packages/svelte/tests/runtime-runes/samples/custom-element-attributes/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/custom-element-attributes/_config.js @@ -20,5 +20,8 @@ export default test({ const [value1, value2] = target.querySelectorAll('value-element'); assert.equal(value1.shadowRoot?.innerHTML, 'test'); assert.equal(value2.shadowRoot?.innerHTML, 'test'); + + const value_builtin = target.querySelector('div'); + assert.equal(value_builtin?.shadowRoot?.innerHTML, 'test'); } }); diff --git a/packages/svelte/tests/runtime-runes/samples/custom-element-attributes/main.svelte b/packages/svelte/tests/runtime-runes/samples/custom-element-attributes/main.svelte index 82774f160d..badb8f96c7 100644 --- a/packages/svelte/tests/runtime-runes/samples/custom-element-attributes/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/custom-element-attributes/main.svelte @@ -15,6 +15,24 @@ } }); } + if(!customElements.get('value-builtin')) { + customElements.define('value-builtin', class extends HTMLDivElement { + + constructor() { + super(); + this.attachShadow({ mode: 'open' }); + } + + set value(v) { + if (this.__value !== v) { + this.__value = v; + this.shadowRoot.innerHTML = `${v}`; + } + } + }, { + extends: 'div' + }); + } @@ -22,3 +40,4 @@ +
diff --git a/packages/svelte/tests/runtime-runes/samples/flush-sync-inside-attachment/Child.svelte b/packages/svelte/tests/runtime-runes/samples/flush-sync-inside-attachment/Child.svelte new file mode 100644 index 0000000000..44447e4f36 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/flush-sync-inside-attachment/Child.svelte @@ -0,0 +1,7 @@ + + +{text} diff --git a/packages/svelte/tests/runtime-runes/samples/flush-sync-inside-attachment/_config.js b/packages/svelte/tests/runtime-runes/samples/flush-sync-inside-attachment/_config.js new file mode 100644 index 0000000000..ec8858b2c6 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/flush-sync-inside-attachment/_config.js @@ -0,0 +1,12 @@ +import { async_mode } from '../../../helpers'; +import { test } from '../../test'; + +export default test({ + // In legacy mode this succeeds and logs 'hello' + // In async mode this throws an error because flushSync is called inside an effect + async test({ assert, target, logs }) { + assert.htmlEqual(target.innerHTML, `
hello
`); + assert.deepEqual(logs, ['hello']); + }, + runtime_error: async_mode ? 'flush_sync_in_effect' : undefined +}); diff --git a/packages/svelte/tests/runtime-runes/samples/flush-sync-inside-attachment/main.svelte b/packages/svelte/tests/runtime-runes/samples/flush-sync-inside-attachment/main.svelte new file mode 100644 index 0000000000..bef820376b --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/flush-sync-inside-attachment/main.svelte @@ -0,0 +1,13 @@ + + + + +
{ + mount(Child, { target, props: { text: 'hello' } }); + flushSync(); +}}>
diff --git a/packages/svelte/tests/validator/samples/const-tag-placement-svelte-boundary/input.svelte b/packages/svelte/tests/validator/samples/const-tag-placement-svelte-boundary/input.svelte index 5708cc36ca..c965a379e5 100644 --- a/packages/svelte/tests/validator/samples/const-tag-placement-svelte-boundary/input.svelte +++ b/packages/svelte/tests/validator/samples/const-tag-placement-svelte-boundary/input.svelte @@ -4,8 +4,6 @@ {@const x = a} - {#snippet failed()} - {x} - {/snippet} + {x} - \ No newline at end of file +