From 0672e48223171f86b309e3de84c1c3846eee0bda Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 14 Jul 2025 15:58:44 -0400 Subject: [PATCH] feat: allow `await` in components (#15844) * tidy * tidy * yes it can, apparently * tidy up * unused * complete merge * WIP * simplify * debugging help * WIP * unused * partial merge * WIP * fix * add test * rename * fix * unused * oops * chore: merge main into async branch (#16197) * chore: merge main into async branch * adjust test * fix: make effects depend on state created inside them (#16198) * make effects depend on state created inside them * fix, add github action * disable test in async mode * make batch.#deferred private * fix settled when awaits occur inside pending boundary * tweak * change behaviour of `tick()` to be requestAnimationFrame-based * get rid of a bunch of Promise.resolve chains * more * more * fix test * disallow `flushSync()` inside effects * regenerate * handle errors in block expressions * make validate_each_keys async-aware * for unowned deriveds, throw errors lazily * rename ASYNC_ERROR -> ERROR_VALUE, and avoid conflicts with other flags now that it's used with deriveds as well as sources * invoke boundary directly * local effect pending * update test * fix * fix * fix weird bug in tests * delete old changeset that somehow got left over here * Update .changeset/eleven-weeks-dance.md * update error details * unused * simplify * tweak * tweak * tweak * tweak * tidy up * handle errors in async block expressions * tweak * groundwork for async attribute_effect * dry out * fix async directives * tidy up * initialize option values before initing select values * simplify init_select * simplify * tweak * tidy up * tweak * on second thoughts just simplify it here * tidy * handle awaits in `` * unused * tidy up * tidy up * dry out * dry out * Revert "dry out" This reverts commit 25855163bf7d915492f62d3d0e5370ed74e65132. * dry out * dry out * use let for block-scoped stuff * dry out * dry out * tidy up * only wrap awaits in `$.save` when necessary * oops * remove TODO comment (just checked) * oops, leftover * simplify * unused * remove logging * tweak * unused * unused * remove logging * partial fix * fix * remove unused EFFECT_HAS_DERIVED * Update packages/svelte/src/reactivity/create-subscriber.js Co-authored-by: Elliott Johnson * Update packages/svelte/src/index-client.js Co-authored-by: Elliott Johnson * Update packages/svelte/src/internal/client/runtime.js Co-authored-by: Elliott Johnson * unused * Update packages/svelte/src/internal/client/reactivity/sources.js Co-authored-by: Elliott Johnson * Update packages/svelte/src/internal/client/reactivity/deriveds.js Co-authored-by: Elliott Johnson * Update packages/svelte/src/internal/client/reactivity/deriveds.js Co-authored-by: Elliott Johnson * prettier * unused * fix flags * tweak * tweak * unused * fix * no idea what a 'boundary micro task' is or why it was deemed necessary but evidently it isn't * remove queue_boundary_micro_task * oops * note * tidy up * remove TODO * make method private * simplify * flesh out await_reactivity_loss warning * tweak * update test * fix * null out from_async_derived in more places * tidy up test * failing test * unused * fix test * fix * simplify. no idea what the async_mode_flag stuff is about, but it appears unnecessary * add async_derived_orphan error * regenerate * flesh out await_outside_boundary message * add some JSDoc * only update `$effect.pending()` if someone is listening, since it causes a double flush and makes debugging harder * tweak logic to make it clearer why and when we commit a batch * add a couple of comments * false -> 0 * add comment * unused * silence warning * add effect_pending_outside_reaction error * Update packages/svelte/src/compiler/types/index.d.ts Co-authored-by: Elliott Johnson * suspend batch, not boundary * rename from_async_derived -> current_async_derived * tweak * remove TODO - this method is only called when pending snippet exists * use error boundary for test - vitest does some weird error swallowing afaict * flush less often * restore -> activate * remove TODO * move batch-related code into batch.js * make flush_queued_root_effects a method of batch * make process_effects a method of batch * make stuff private * unused * regenerate * update test * more JSDoc * add more JSDoc * branch and block effects do not also need to be render effects * tidy up * simplify * unused * move code where it belongs * remove, for now * fix * only apply error adjustments when error escapes boundaries * remove EFFECT_IS_UPDATING * is_dirty is a better name than check_dirtiness * duplicates are rare and harmless * apparently we no longer need the merging logic? we can simplify and fix stuff by removing it * tidy * don't commit stale batches * add skipped failing test * partial merge * WIP * WIP * WIP * tweak * tidy up * dont update derived status when time-travelling * tidy up * tidy up * tag async deriveds * tweak * bail out of secondary flushes * re-run blocks on subsequent flushes * add test * fix * add tests, one failing * fix * flesh out await_waterfall message * tidy up * dry out * unused * tweak * tidy up * TODO * tweak * tidy up * remove TODO * unused export * add optimisation back * revert unneeded changes * revert * update some tests * more * more * move some code * rename * WIP * unset context synchronously * remove unused argument * Apply suggestions from code review Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com> * add comment * add comment * use queue_micro_task in createSubscriber * Update packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitExpression.js Co-authored-by: Elliott Johnson * prettier --------- Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com> Co-authored-by: Simon Holthausen Co-authored-by: Elliott Johnson --- .changeset/eleven-weeks-dance.md | 5 + .github/workflows/ci.yml | 17 + .../98-reference/.generated/client-errors.md | 36 ++ .../.generated/client-warnings.md | 61 ++ .../98-reference/.generated/compile-errors.md | 12 + .../98-reference/.generated/shared-errors.md | 20 + eslint.config.js | 3 +- .../svelte/messages/client-errors/errors.md | 28 + .../messages/client-warnings/warnings.md | 57 ++ .../svelte/messages/compile-errors/script.md | 8 + .../svelte/messages/shared-errors/errors.md | 18 + packages/svelte/package.json | 3 + packages/svelte/src/compiler/errors.js | 18 + packages/svelte/src/compiler/migrate/index.js | 5 +- .../compiler/phases/1-parse/state/element.js | 2 + .../src/compiler/phases/2-analyze/index.js | 32 +- .../2-analyze/visitors/AwaitExpression.js | 30 + .../2-analyze/visitors/CallExpression.js | 22 +- .../2-analyze/visitors/StyleDirective.js | 1 + .../2-analyze/visitors/SvelteBoundary.js | 2 +- .../2-analyze/visitors/SvelteElement.js | 14 +- .../3-transform/client/transform-client.js | 70 +- .../phases/3-transform/client/types.d.ts | 9 +- .../client/visitors/AwaitExpression.js | 126 ++++ .../client/visitors/CallExpression.js | 10 +- .../3-transform/client/visitors/EachBlock.js | 31 +- .../3-transform/client/visitors/Fragment.js | 4 +- .../client/visitors/FunctionDeclaration.js | 2 +- .../3-transform/client/visitors/HtmlTag.js | 19 +- .../3-transform/client/visitors/IfBlock.js | 42 +- .../3-transform/client/visitors/KeyBlock.js | 28 +- .../client/visitors/RegularElement.js | 21 +- .../3-transform/client/visitors/RenderTag.js | 59 +- .../client/visitors/SlotElement.js | 22 +- .../client/visitors/SvelteBoundary.js | 4 +- .../client/visitors/SvelteElement.js | 41 +- .../client/visitors/VariableDeclaration.js | 44 +- .../client/visitors/shared/component.js | 46 +- .../client/visitors/shared/element.js | 25 +- .../client/visitors/shared/function.js | 2 +- .../client/visitors/shared/utils.js | 35 +- .../3-transform/server/transform-server.js | 2 + .../server/visitors/AwaitExpression.js | 25 + .../server/visitors/CallExpression.js | 4 + .../3-transform/server/visitors/IfBlock.js | 27 +- .../server/visitors/SvelteBoundary.js | 29 +- packages/svelte/src/compiler/phases/nodes.js | 3 +- packages/svelte/src/compiler/phases/scope.js | 19 + .../svelte/src/compiler/phases/types.d.ts | 9 +- packages/svelte/src/compiler/types/index.d.ts | 7 + .../svelte/src/compiler/types/template.d.ts | 1 + .../svelte/src/compiler/utils/builders.js | 36 +- .../svelte/src/compiler/validate-options.js | 6 +- packages/svelte/src/constants.js | 2 + packages/svelte/src/index-client.js | 4 +- packages/svelte/src/index-server.js | 2 + .../svelte/src/internal/client/constants.js | 9 +- .../svelte/src/internal/client/context.js | 28 +- .../svelte/src/internal/client/dev/debug.js | 3 + .../svelte/src/internal/client/dev/tracing.js | 4 +- .../src/internal/client/dom/blocks/async.js | 26 + .../src/internal/client/dom/blocks/await.js | 3 +- .../internal/client/dom/blocks/boundary.js | 207 +++++- .../src/internal/client/dom/blocks/each.js | 202 ++++-- .../src/internal/client/dom/blocks/if.js | 119 ++-- .../src/internal/client/dom/blocks/key.js | 44 +- .../client/dom/blocks/svelte-component.js | 43 +- .../client/dom/elements/attributes.js | 81 +-- .../client/dom/elements/bindings/input.js | 9 + .../src/internal/client/dom/operations.js | 19 +- .../src/internal/client/error-handling.js | 21 +- packages/svelte/src/internal/client/errors.js | 64 ++ packages/svelte/src/internal/client/index.js | 15 +- .../src/internal/client/reactivity/async.js | 127 ++++ .../src/internal/client/reactivity/batch.js | 603 ++++++++++++++++++ .../internal/client/reactivity/deriveds.js | 172 ++++- .../src/internal/client/reactivity/effects.js | 97 +-- .../src/internal/client/reactivity/sources.js | 11 +- .../svelte/src/internal/client/runtime.js | 462 +++++--------- .../svelte/src/internal/client/types.d.ts | 4 +- .../svelte/src/internal/client/warnings.js | 25 + packages/svelte/src/internal/flags/async.js | 3 + packages/svelte/src/internal/flags/index.js | 10 + packages/svelte/src/internal/server/index.js | 2 + packages/svelte/src/internal/shared/errors.js | 16 + packages/svelte/src/legacy/legacy-client.js | 9 +- .../src/reactivity/create-subscriber.js | 5 +- packages/svelte/src/utils.js | 1 + packages/svelte/tests/helpers.js | 5 +- .../samples/transition-abort/_config.js | 2 + .../svelte/tests/runtime-legacy/shared.ts | 46 +- .../samples/async-abort-signal/_config.js | 23 + .../samples/async-abort-signal/main.svelte | 29 + .../async-attribute-without-state/_config.js | 18 + .../async-attribute-without-state/main.svelte | 7 + .../samples/async-attribute/_config.js | 29 + .../samples/async-attribute/main.svelte | 15 + .../_config.js | 27 + .../main.svelte | 28 + .../async-block-reject-during-init/_config.js | 10 + .../main.svelte | 8 + .../_config.js | 28 + .../main.svelte | 29 + .../samples/async-block-rerun/_config.js | 53 ++ .../samples/async-block-rerun/main.svelte | 45 ++ .../samples/async-child-effect/_config.js | 55 ++ .../samples/async-child-effect/main.svelte | 26 + .../samples/async-class-directive/_config.js | 20 + .../samples/async-class-directive/main.svelte | 11 + .../samples/async-derived-in-if/Child.svelte | 5 + .../samples/async-derived-in-if/_config.js | 19 + .../samples/async-derived-in-if/main.svelte | 17 + .../Child.svelte | 9 + .../_config.js | 33 + .../main.svelte | 20 + .../samples/async-derived-module/Child.svelte | 20 + .../samples/async-derived-module/_config.js | 93 +++ .../samples/async-derived-module/main.svelte | 21 + .../async-derived-module/state.svelte.js | 9 + .../async-derived-unchanging/Component.svelte | 29 + .../async-derived-unchanging/_config.js | 33 + .../async-derived-unchanging/main.svelte | 11 + .../samples/async-derived/Child.svelte | 15 + .../samples/async-derived/_config.js | 48 ++ .../samples/async-derived/main.svelte | 21 + .../samples/async-each-await-item/_config.js | 31 + .../samples/async-each-await-item/main.svelte | 39 ++ .../samples/async-each-keyed/_config.js | 41 ++ .../samples/async-each-keyed/main.svelte | 24 + .../samples/async-each/_config.js | 48 ++ .../samples/async-each/main.svelte | 17 + .../_config.js | 11 + .../main.svelte | 8 + .../samples/async-error-recovery/_config.js | 87 +++ .../samples/async-error-recovery/main.svelte | 24 + .../samples/async-error/_config.js | 34 + .../samples/async-error/main.svelte | 20 + .../samples/async-expression/_config.js | 56 ++ .../samples/async-expression/main.svelte | 19 + .../samples/async-html-tag/_config.js | 51 ++ .../samples/async-html-tag/main.svelte | 15 + .../runtime-runes/samples/async-if/_config.js | 31 + .../samples/async-if/main.svelte | 19 + .../samples/async-key/_config.js | 46 ++ .../samples/async-key/main.svelte | 17 + .../_config.js | 35 + .../main.svelte | 17 + .../_config.js | 30 + .../main.svelte | 31 + .../samples/async-nested-derived/Child.svelte | 11 + .../samples/async-nested-derived/_config.js | 14 + .../samples/async-nested-derived/main.svelte | 15 + .../samples/async-prop/Child.svelte | 5 + .../samples/async-prop/_config.js | 51 ++ .../samples/async-prop/main.svelte | 17 + .../samples/async-reactivity-loss/_config.js | 24 + .../samples/async-reactivity-loss/main.svelte | 20 + .../samples/async-redirect-initial/_config.js | 51 ++ .../async-redirect-initial/main.svelte | 43 ++ .../samples/async-redirect/_config.js | 52 ++ .../samples/async-redirect/main.svelte | 43 ++ .../samples/async-render-tag/_config.js | 51 ++ .../samples/async-render-tag/main.svelte | 19 + .../samples/async-slot/Child.svelte | 1 + .../samples/async-slot/_config.js | 13 + .../samples/async-slot/main.svelte | 13 + .../samples/async-stale-derived-2/_config.js | 58 ++ .../samples/async-stale-derived-2/main.svelte | 34 + .../samples/async-stale-derived/_config.js | 52 ++ .../samples/async-stale-derived/main.svelte | 26 + .../samples/async-svelte-element/_config.js | 51 ++ .../samples/async-svelte-element/main.svelte | 15 + .../async-top-level-in-if/Child.svelte | 7 + .../samples/async-top-level-in-if/_config.js | 40 ++ .../samples/async-top-level-in-if/main.svelte | 20 + .../samples/async-top-level/Child.svelte | 7 + .../samples/async-top-level/_config.js | 14 + .../samples/async-top-level/main.svelte | 15 + .../async-unresolved-promise/_config.js | 32 + .../async-unresolved-promise/main.svelte | 19 + .../async-waterfall-on-init/_config.js | 42 ++ .../async-waterfall-on-init/main.svelte | 22 + .../async-with-sync-derived/_config.js | 53 ++ .../async-with-sync-derived/main.svelte | 19 + .../samples/effect-cleanup/_config.js | 9 +- .../samples/effect-cleanup/main.svelte | 2 +- .../samples/error-boundary-18/_config.js | 2 + .../set-context-after-await/Child.svelte | 11 + .../set-context-after-await/_config.js | 12 + .../set-context-after-await/main.svelte | 11 + .../set-context-after-mount/_config.js | 16 + .../set-context-after-mount/main.svelte | 20 + .../samples/tick-timing/_config.js | 2 + .../samples/untrack-own-deriveds/_config.js | 2 + packages/svelte/tests/signals/test.ts | 76 ++- packages/svelte/tests/store/test.ts | 9 +- packages/svelte/tests/suite.ts | 6 +- packages/svelte/types/index.d.ts | 61 +- playgrounds/sandbox/index.html | 6 + playgrounds/sandbox/run.js | 15 +- playgrounds/sandbox/ssr-common.js | 17 + playgrounds/sandbox/ssr-dev.js | 1 + playgrounds/sandbox/ssr-prod.js | 1 + playgrounds/sandbox/svelte.config.js | 6 +- 204 files changed, 5777 insertions(+), 799 deletions(-) create mode 100644 .changeset/eleven-weeks-dance.md create mode 100644 packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js create mode 100644 packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitExpression.js create mode 100644 packages/svelte/src/compiler/phases/3-transform/server/visitors/AwaitExpression.js create mode 100644 packages/svelte/src/internal/client/dom/blocks/async.js create mode 100644 packages/svelte/src/internal/client/reactivity/async.js create mode 100644 packages/svelte/src/internal/client/reactivity/batch.js create mode 100644 packages/svelte/src/internal/flags/async.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-abort-signal/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-abort-signal/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-attribute-without-state/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-attribute-without-state/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-attribute/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-attribute/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-block-destroy-during-init/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-block-destroy-during-init/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-block-reject-during-init/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-block-reject-during-init/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-block-reject-each-during-init/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-block-reject-each-during-init/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-block-rerun/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-block-rerun/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-child-effect/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-child-effect/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-class-directive/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-class-directive/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-derived-in-if/Child.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-derived-in-if/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-derived-in-if/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-derived-invalidation-during-init/Child.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-derived-invalidation-during-init/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-derived-invalidation-during-init/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-derived-module/Child.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-derived-module/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-derived-module/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-derived-module/state.svelte.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-derived-unchanging/Component.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-derived-unchanging/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-derived-unchanging/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-derived/Child.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-derived/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-derived/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-each-await-item/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-each-await-item/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-each-keyed/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-each-keyed/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-each/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-each/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-error-in-block-expression/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-error-in-block-expression/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-error-recovery/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-error-recovery/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-error/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-error/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-expression/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-expression/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-html-tag/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-html-tag/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-if/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-if/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-key/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-key/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-linear-order-different-deriveds/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-linear-order-different-deriveds/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-linear-order-same-derived/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-linear-order-same-derived/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-nested-derived/Child.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-nested-derived/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-nested-derived/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-prop/Child.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-prop/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-prop/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-reactivity-loss/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-reactivity-loss/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-redirect-initial/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-redirect-initial/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-redirect/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-redirect/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-render-tag/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-render-tag/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-slot/Child.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-slot/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-slot/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-stale-derived-2/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-stale-derived-2/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-stale-derived/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-stale-derived/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-svelte-element/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-svelte-element/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-top-level-in-if/Child.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-top-level-in-if/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-top-level-in-if/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-top-level/Child.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-top-level/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-top-level/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-unresolved-promise/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-unresolved-promise/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-waterfall-on-init/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-waterfall-on-init/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-with-sync-derived/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-with-sync-derived/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/set-context-after-await/Child.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/set-context-after-await/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/set-context-after-await/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/set-context-after-mount/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/set-context-after-mount/main.svelte create mode 100644 playgrounds/sandbox/ssr-common.js diff --git a/.changeset/eleven-weeks-dance.md b/.changeset/eleven-weeks-dance.md new file mode 100644 index 0000000000..91245df0eb --- /dev/null +++ b/.changeset/eleven-weeks-dance.md @@ -0,0 +1,5 @@ +--- +'svelte': minor +--- + +feat: support `await` in components when using the `experimental.async` compiler option diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c0e1d36760..046ad335f3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -43,6 +43,23 @@ jobs: - run: pnpm test env: CI: true + TestNoAsync: + permissions: {} + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: pnpm + - run: pnpm install --frozen-lockfile + - run: pnpm playwright install chromium + - run: pnpm test runtime-runes + env: + CI: true + SVELTE_NO_ASYNC: true Lint: permissions: {} runs-on: ubuntu-latest diff --git a/documentation/docs/98-reference/.generated/client-errors.md b/documentation/docs/98-reference/.generated/client-errors.md index 3f33e37d2e..c7d6ec8ac9 100644 --- a/documentation/docs/98-reference/.generated/client-errors.md +++ b/documentation/docs/98-reference/.generated/client-errors.md @@ -1,5 +1,17 @@ +### async_derived_orphan + +``` +Cannot create a `$derived(...)` with an `await` expression outside of an effect tree +``` + +In Svelte there are two types of reaction — [`$derived`](/docs/svelte/$derived) and [`$effect`](/docs/svelte/$effect). Deriveds can be created anywhere, because they run _lazily_ and can be [garbage collected](https://developer.mozilla.org/en-US/docs/Glossary/Garbage_collection) if nothing references them. Effects, by contrast, keep running eagerly whenever their dependencies change, until they are destroyed. + +Because of this, effects can only be created inside other effects (or [effect roots](/docs/svelte/$effect#$effect.root), such as the one that is created when you first mount a component) so that Svelte knows when to destroy them. + +Some sleight of hand occurs when a derived contains an `await` expression: Since waiting until we read `{await getPromise()}` to call `getPromise` would be too late, we use an effect to instead call it proactively, notifying Svelte when the value is available. But since we're using an effect, we can only create asynchronous deriveds inside another effect. + ### bind_invalid_checkbox_value ``` @@ -68,12 +80,28 @@ Effect cannot be created inside a `$derived` value that was not itself created i `%rune%` can only be used inside an effect (e.g. during component initialisation) ``` +### effect_pending_outside_reaction + +``` +`$effect.pending()` can only be called inside an effect or derived +``` + ### effect_update_depth_exceeded ``` Maximum update depth exceeded. This can happen when a reactive block or effect repeatedly sets a new value. Svelte limits the number of nested updates to prevent infinite loops ``` +### flush_sync_in_effect + +``` +Cannot use `flushSync` inside an effect +``` + +The `flushSync()` function can be used to flush any pending effects synchronously. It cannot be used if effects are currently being flushed — in other words, you can call it after a state change but _not_ inside an effect. + +This restriction only applies when using the `experimental.async` option, which will be active by default in Svelte 6. + ### get_abort_signal_outside_reaction ``` @@ -116,6 +144,14 @@ Rest element properties of `$props()` such as `%property%` are readonly The `%rune%` rune is only available inside `.svelte` and `.svelte.js/ts` files ``` +### set_context_after_init + +``` +`setContext` must be called when a component first initializes, not in a subsequent effect or after an `await` expression +``` + +This restriction only applies when using the `experimental.async` option, which will be active by default in Svelte 6. + ### state_descriptors_fixed ``` diff --git a/documentation/docs/98-reference/.generated/client-warnings.md b/documentation/docs/98-reference/.generated/client-warnings.md index fe90b0db38..1c75faef53 100644 --- a/documentation/docs/98-reference/.generated/client-warnings.md +++ b/documentation/docs/98-reference/.generated/client-warnings.md @@ -34,6 +34,67 @@ function add() { } ``` +### await_reactivity_loss + +``` +Detected reactivity loss when reading `%name%`. This happens when state is read in an async function after an earlier `await` +``` + +Svelte's signal-based reactivity works by tracking which bits of state are read when a template or `$derived(...)` expression executes. If an expression contains an `await`, Svelte transforms it such that any state _after_ the `await` is also tracked — in other words, in a case like this... + +```js +let total = $derived(await a + b); +``` + +...both `a` and `b` are tracked, even though `b` is only read once `a` has resolved, after the initial execution. + +This does _not_ apply to an `await` that is not 'visible' inside the expression. In a case like this... + +```js +async function sum() { + return await a + b; +} + +let total = $derived(await sum()); +``` + +...`total` will depend on `a` (which is read immediately) but not `b` (which is not). The solution is to pass the values into the function: + +```js +async function sum(a, b) { + return await a + b; +} + +let total = $derived(await sum(a, b)); +``` + +### await_waterfall + +``` +An async derived, `%name%` (%location%) was not read immediately after it resolved. This often indicates an unnecessary waterfall, which can slow down your app +``` + +In a case like this... + +```js +let a = $derived(await one()); +let b = $derived(await two()); +``` + +...the second `$derived` will not be created until the first one has resolved. Since `await two()` does not depend on the value of `a`, this delay, often described as a 'waterfall', is unnecessary. + +(Note that if the values of `await one()` and `await two()` subsequently change, they can do so concurrently — the waterfall only occurs when the deriveds are first created.) + +You can solve this by creating the promises first and _then_ awaiting them: + +```js +let aPromise = $derived(one()); +let bPromise = $derived(two()); + +let a = $derived(await aPromise); +let b = $derived(await bPromise); +``` + ### binding_property_non_reactive ``` diff --git a/documentation/docs/98-reference/.generated/compile-errors.md b/documentation/docs/98-reference/.generated/compile-errors.md index db848a0299..20f57770d1 100644 --- a/documentation/docs/98-reference/.generated/compile-errors.md +++ b/documentation/docs/98-reference/.generated/compile-errors.md @@ -480,6 +480,12 @@ Expected token %token% Expected whitespace ``` +### experimental_async + +``` +Cannot use `await` in deriveds and template expressions, or at the top level of a component, unless the `experimental.async` compiler option is `true` +``` + ### export_undefined ``` @@ -534,6 +540,12 @@ The arguments keyword cannot be used within the template or at the top level of %message% ``` +### legacy_await_invalid + +``` +Cannot use `await` in deriveds and template expressions, or at the top level of a component, unless in runes mode +``` + ### legacy_export_invalid ``` diff --git a/documentation/docs/98-reference/.generated/shared-errors.md b/documentation/docs/98-reference/.generated/shared-errors.md index 6c31aaafd0..de34b3f5da 100644 --- a/documentation/docs/98-reference/.generated/shared-errors.md +++ b/documentation/docs/98-reference/.generated/shared-errors.md @@ -1,5 +1,25 @@ +### await_outside_boundary + +``` +Cannot await outside a `` with a `pending` snippet +``` + +The `await` keyword can only appear in a `$derived(...)` or template expression, or at the top level of a component's ` + + + + + +

{await load(deferred)}

+ + {#snippet pending()} +

pending

+ {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-attribute-without-state/_config.js b/packages/svelte/tests/runtime-runes/samples/async-attribute-without-state/_config.js new file mode 100644 index 0000000000..3de81a507b --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-attribute-without-state/_config.js @@ -0,0 +1,18 @@ +import { flushSync, tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: ` +

pending

+ `, + + async test({ assert, target }) { + await Promise.resolve(); + await Promise.resolve(); + await Promise.resolve(); + await Promise.resolve(); + flushSync(); + + assert.htmlEqual(target.innerHTML, '

hello

'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-attribute-without-state/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-attribute-without-state/main.svelte new file mode 100644 index 0000000000..00a11cac43 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-attribute-without-state/main.svelte @@ -0,0 +1,7 @@ + +

hello

+ + {#snippet pending()} +

pending

+ {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-attribute/_config.js b/packages/svelte/tests/runtime-runes/samples/async-attribute/_config.js new file mode 100644 index 0000000000..0a64738409 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-attribute/_config.js @@ -0,0 +1,29 @@ +import { tick } from 'svelte'; +import { ok, test } from '../../test'; + +export default test({ + html: ` + + + +

pending

+ `, + + async test({ assert, target }) { + const [cool, neat, reset] = target.querySelectorAll('button'); + + cool.click(); + await tick(); + + const p = target.querySelector('p'); + ok(p); + assert.htmlEqual(p.outerHTML, '

hello

'); + + reset.click(); + assert.htmlEqual(p.outerHTML, '

hello

'); + + neat.click(); + await tick(); + assert.htmlEqual(p.outerHTML, '

hello

'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-attribute/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-attribute/main.svelte new file mode 100644 index 0000000000..6332a9802d --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-attribute/main.svelte @@ -0,0 +1,15 @@ + + + + + + + +

hello

+ + {#snippet pending()} +

pending

+ {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-block-destroy-during-init/_config.js b/packages/svelte/tests/runtime-runes/samples/async-block-destroy-during-init/_config.js new file mode 100644 index 0000000000..a29c99860d --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-block-destroy-during-init/_config.js @@ -0,0 +1,27 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + const [increment, shift] = target.querySelectorAll('button'); + + increment.click(); + await tick(); + + shift.click(); + await tick(); + + shift.click(); + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` + + +

false

+

1

+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-block-destroy-during-init/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-block-destroy-during-init/main.svelte new file mode 100644 index 0000000000..a93eb7dc25 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-block-destroy-during-init/main.svelte @@ -0,0 +1,28 @@ + + + + + + + {#if count % 2 === 0} +

true

+

{await push()}

+ {:else} +

false

+

{await push()}

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

loading...

+ {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-block-reject-during-init/_config.js b/packages/svelte/tests/runtime-runes/samples/async-block-reject-during-init/_config.js new file mode 100644 index 0000000000..e2718a35d2 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-block-reject-during-init/_config.js @@ -0,0 +1,10 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + assert.htmlEqual(target.innerHTML, 'loading'); + await tick(); + assert.htmlEqual(target.innerHTML, 'nope'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-block-reject-during-init/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-block-reject-during-init/main.svelte new file mode 100644 index 0000000000..412da7268e --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-block-reject-during-init/main.svelte @@ -0,0 +1,8 @@ + + {#if await Promise.reject(new Error('nope'))} + hi + {/if} + + {#snippet pending()}loading{/snippet} + {#snippet failed(e)}{e.message}{/snippet} + diff --git a/packages/svelte/tests/runtime-runes/samples/async-block-reject-each-during-init/_config.js b/packages/svelte/tests/runtime-runes/samples/async-block-reject-each-during-init/_config.js new file mode 100644 index 0000000000..c5dae7fee2 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-block-reject-each-during-init/_config.js @@ -0,0 +1,28 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + const [increment, resolve, reject] = target.querySelectorAll('button'); + + increment.click(); + await tick(); + + reject.click(); + await tick(); + + resolve.click(); + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` + + + +

false

+

1

+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-block-reject-each-during-init/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-block-reject-each-during-init/main.svelte new file mode 100644 index 0000000000..1ad6cb84de --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-block-reject-each-during-init/main.svelte @@ -0,0 +1,29 @@ + + + + + + + + {#if count % 2 === 0} +

true

+ {#each await push() as count}

{count}

{/each} + {:else} +

false

+ {#each await push() as count}

{count}

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

loading...

+ {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-block-rerun/_config.js b/packages/svelte/tests/runtime-runes/samples/async-block-rerun/_config.js new file mode 100644 index 0000000000..18175de4dc --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-block-rerun/_config.js @@ -0,0 +1,53 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + const [override, release, resolve] = target.querySelectorAll('button'); + + resolve.click(); + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` + + + +

before

+

before

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

during

+

during

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

after

+

after

+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-block-rerun/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-block-rerun/main.svelte new file mode 100644 index 0000000000..256ad68f4a --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-block-rerun/main.svelte @@ -0,0 +1,45 @@ + + + + + + + + + + {#each await indirect() as entry} +

{entry}

+ {/each} + + {#each current as entry} +

{entry}

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

pending...

+ {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-child-effect/_config.js b/packages/svelte/tests/runtime-runes/samples/async-child-effect/_config.js new file mode 100644 index 0000000000..325cb1dcd6 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-child-effect/_config.js @@ -0,0 +1,55 @@ +import { flushSync, tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: ` + +

loading

+ `, + + async test({ assert, target }) { + target.querySelector('button')?.click(); + await tick(); + + const [button1, button2] = target.querySelectorAll('button'); + + assert.htmlEqual( + target.innerHTML, + ` + + +

A

+

a

+ ` + ); + + flushSync(() => button2.click()); + flushSync(() => button2.click()); + + button1.click(); + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` + + +

AA

+

aa

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

AAA

+

aaa

+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-child-effect/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-child-effect/main.svelte new file mode 100644 index 0000000000..edb0eaea44 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-child-effect/main.svelte @@ -0,0 +1,26 @@ + + + + + + +

{await push(input.toUpperCase())}

+ + {#if true} +

{input}

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

loading

+ {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-class-directive/_config.js b/packages/svelte/tests/runtime-runes/samples/async-class-directive/_config.js new file mode 100644 index 0000000000..3186ed2069 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-class-directive/_config.js @@ -0,0 +1,20 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: `loading`, + + async test({ assert, target }) { + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` +
one
+
two
+
red
+
blue
+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-class-directive/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-class-directive/main.svelte new file mode 100644 index 0000000000..f0f27e4830 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-class-directive/main.svelte @@ -0,0 +1,11 @@ + +
one
+
two
+ +
red
+
blue
+ + {#snippet pending()} + loading + {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-in-if/Child.svelte b/packages/svelte/tests/runtime-runes/samples/async-derived-in-if/Child.svelte new file mode 100644 index 0000000000..fb47377513 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-derived-in-if/Child.svelte @@ -0,0 +1,5 @@ + + +

{n}

diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-in-if/_config.js b/packages/svelte/tests/runtime-runes/samples/async-derived-in-if/_config.js new file mode 100644 index 0000000000..914b311c97 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-derived-in-if/_config.js @@ -0,0 +1,19 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + const button = target.querySelector('button'); + + button?.click(); + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` + +

1

+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-in-if/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-derived-in-if/main.svelte new file mode 100644 index 0000000000..a53381c2d5 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-derived-in-if/main.svelte @@ -0,0 +1,17 @@ + + + + + + {#if show} + + {/if} + + {#snippet pending()} +

pending

+ {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-invalidation-during-init/Child.svelte b/packages/svelte/tests/runtime-runes/samples/async-derived-invalidation-during-init/Child.svelte new file mode 100644 index 0000000000..ffcd8b46b4 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-derived-invalidation-during-init/Child.svelte @@ -0,0 +1,9 @@ + + +

{(await d).value}

diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-invalidation-during-init/_config.js b/packages/svelte/tests/runtime-runes/samples/async-derived-invalidation-during-init/_config.js new file mode 100644 index 0000000000..fee8e2e6bf --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-derived-invalidation-during-init/_config.js @@ -0,0 +1,33 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: ` + + + +

pending

+ `, + + async test({ assert, target, errors }) { + const [toggle, resolve1, resolve2] = target.querySelectorAll('button'); + + toggle.click(); + resolve1.click(); + resolve2.click(); + + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` + + + +

two

+ ` + ); + + assert.deepEqual(errors, []); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-invalidation-during-init/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-derived-invalidation-during-init/main.svelte new file mode 100644 index 0000000000..9babdb2fe2 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-derived-invalidation-during-init/main.svelte @@ -0,0 +1,20 @@ + + + + + + + + + + {#snippet pending()} +

pending

+ {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-module/Child.svelte b/packages/svelte/tests/runtime-runes/samples/async-derived-module/Child.svelte new file mode 100644 index 0000000000..f803a30c37 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-derived-module/Child.svelte @@ -0,0 +1,20 @@ + + +

{derived.value}{console.log(`template ${derived.value} ${num}`)}

diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-module/_config.js b/packages/svelte/tests/runtime-runes/samples/async-derived-module/_config.js new file mode 100644 index 0000000000..f7d1d28fde --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-derived-module/_config.js @@ -0,0 +1,93 @@ +import { flushSync, tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: ` + + + + +

pending

+ `, + + async test({ assert, target, logs }) { + const [reset, a, b, increment] = target.querySelectorAll('button'); + + a.click(); + + // TODO why is this necessary? why isn't `await tick()` enough? + await Promise.resolve(); + await Promise.resolve(); + await Promise.resolve(); + await Promise.resolve(); + await Promise.resolve(); + await Promise.resolve(); + await Promise.resolve(); + await Promise.resolve(); + flushSync(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + + +

42

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

84

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

84

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

86

+ ` + ); + + assert.deepEqual(logs, [ + 'outside boundary 1', + '$effect.pre 42 1', + 'template 42 1', + '$effect 42 1', + '$effect.pre 84 2', + 'template 84 2', + 'outside boundary 2', + '$effect 84 2', + '$effect.pre 86 2', + 'template 86 2', + '$effect 86 2' + ]); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-module/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-derived-module/main.svelte new file mode 100644 index 0000000000..2c83e1d23d --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-derived-module/main.svelte @@ -0,0 +1,21 @@ + + + + + + + + + + + {#snippet pending()} +

pending

+ {/snippet} +
+ +{console.log(`outside boundary ${num}`)} diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-module/state.svelte.js b/packages/svelte/tests/runtime-runes/samples/async-derived-module/state.svelte.js new file mode 100644 index 0000000000..a53fbb8c6f --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-derived-module/state.svelte.js @@ -0,0 +1,9 @@ +export async function create_derived(get_promise, get_num) { + let value = $derived((await get_promise()) * get_num()); + + return { + get value() { + return value; + } + }; +} diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-unchanging/Component.svelte b/packages/svelte/tests/runtime-runes/samples/async-derived-unchanging/Component.svelte new file mode 100644 index 0000000000..a90a9dedf7 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-derived-unchanging/Component.svelte @@ -0,0 +1,29 @@ + + + + + +

{n}: {Math.min(current, 3)}

diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-unchanging/_config.js b/packages/svelte/tests/runtime-runes/samples/async-derived-unchanging/_config.js new file mode 100644 index 0000000000..016c311f98 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-derived-unchanging/_config.js @@ -0,0 +1,33 @@ +import { flushSync, tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: `

pending...

`, + + async test({ assert, target }) { + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` + + +

0: 0

+ ` + ); + + const [shift, increment] = target.querySelectorAll('button'); + const [p] = target.querySelectorAll('p'); + + for (let i = 1; i < 5; i += 1) { + flushSync(() => increment.click()); + } + + for (let i = 1; i < 5; i += 1) { + shift.click(); + await tick(); + + assert.equal(p.innerHTML, `${i}: ${Math.min(i, 3)}`); + } + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-unchanging/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-derived-unchanging/main.svelte new file mode 100644 index 0000000000..2d5ddca4db --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-derived-unchanging/main.svelte @@ -0,0 +1,11 @@ + + + + + + {#snippet pending()} +

pending...

+ {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived/Child.svelte b/packages/svelte/tests/runtime-runes/samples/async-derived/Child.svelte new file mode 100644 index 0000000000..b59fd7c08f --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-derived/Child.svelte @@ -0,0 +1,15 @@ + + +

{value}{console.log(`template ${value} ${num}`)}

diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived/_config.js b/packages/svelte/tests/runtime-runes/samples/async-derived/_config.js new file mode 100644 index 0000000000..7239643464 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-derived/_config.js @@ -0,0 +1,48 @@ +import { flushSync, settled, tick } from 'svelte'; +import { ok, test } from '../../test'; + +export default test({ + html: ` + + + + +

pending

+ `, + + async test({ assert, target, logs }) { + const [resolve_a, resolve_b, reset, increment] = target.querySelectorAll('button'); + + flushSync(() => resolve_a.click()); + await tick(); + + const p = target.querySelector('p'); + ok(p); + assert.htmlEqual(p.innerHTML, '1a'); + + flushSync(() => increment.click()); + await tick(); + assert.htmlEqual(p.innerHTML, '2a'); + + reset.click(); + assert.htmlEqual(p.innerHTML, '2a'); + + resolve_b.click(); + await tick(); + assert.htmlEqual(p.innerHTML, '2b'); + + assert.deepEqual(logs, [ + 'outside boundary 1', + '$effect.pre 1a 1', + 'template 1a 1', + '$effect 1a 1', + '$effect.pre 2a 2', + 'template 2a 2', + 'outside boundary 2', + '$effect 2a 2', + '$effect.pre 2b 2', + 'template 2b 2', + '$effect 2b 2' + ]); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-derived/main.svelte new file mode 100644 index 0000000000..1404ae0299 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-derived/main.svelte @@ -0,0 +1,21 @@ + + + + + + + + + + + {#snippet pending()} +

pending

+ {/snippet} +
+ +{console.log(`outside boundary ${num}`)} diff --git a/packages/svelte/tests/runtime-runes/samples/async-each-await-item/_config.js b/packages/svelte/tests/runtime-runes/samples/async-each-await-item/_config.js new file mode 100644 index 0000000000..54aa68eeb2 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-each-await-item/_config.js @@ -0,0 +1,31 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: `

pending

`, + + async test({ assert, target }) { + const [button1, button2, button3] = target.querySelectorAll('button'); + + button1.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + '

a

b

c

' + ); + + button2.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + '

a

b

c

' + ); + + button3.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + '

b

c

d

e

' + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-each-await-item/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-each-await-item/main.svelte new file mode 100644 index 0000000000..eddcf2b749 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-each-await-item/main.svelte @@ -0,0 +1,39 @@ + + + + + + + + + + {#each items as deferred} +

{await deferred.promise}

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

pending

+ {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-each-keyed/_config.js b/packages/svelte/tests/runtime-runes/samples/async-each-keyed/_config.js new file mode 100644 index 0000000000..43d3a0f876 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-each-keyed/_config.js @@ -0,0 +1,41 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + compileOptions: { + dev: true + }, + + html: ` + + + + +

pending

+ `, + + async test({ assert, target }) { + const [reset, one, two, three] = target.querySelectorAll('button'); + + one.click(); + await tick(); + + const [div] = target.querySelectorAll('div'); + assert.htmlEqual(div.innerHTML, '

a

b

c

'); + + reset.click(); + await tick(); + assert.htmlEqual(div.innerHTML, '

a

b

c

'); + + two.click(); + await tick(); + assert.htmlEqual(div.innerHTML, '

d

e

f

g

'); + + reset.click(); + await tick(); + three.click(); + await tick(); + + assert.include(target.innerHTML, '

each_key_duplicate'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-each-keyed/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-each-keyed/main.svelte new file mode 100644 index 0000000000..e2f8263780 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-each-keyed/main.svelte @@ -0,0 +1,24 @@ + + + + + + + + +

+ {#each await deferred.promise as item (item)} +

{item}

+ {/each} +
+ + {#snippet failed(e)} +

{e.message}

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

pending

+ {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-each/_config.js b/packages/svelte/tests/runtime-runes/samples/async-each/_config.js new file mode 100644 index 0000000000..50aa055130 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-each/_config.js @@ -0,0 +1,48 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: ` + + + +

pending

+ `, + + async test({ assert, target }) { + const [reset, abc, defg] = target.querySelectorAll('button'); + + abc.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + +

a

b

c

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

a

b

c

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

d

e

f

g

` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-each/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-each/main.svelte new file mode 100644 index 0000000000..8e4412811a --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-each/main.svelte @@ -0,0 +1,17 @@ + + + + + + + + {#each await deferred.promise as item} +

{item}

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

pending

+ {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-error-in-block-expression/_config.js b/packages/svelte/tests/runtime-runes/samples/async-error-in-block-expression/_config.js new file mode 100644 index 0000000000..2679785cff --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-error-in-block-expression/_config.js @@ -0,0 +1,11 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: `loading`, + + async test({ assert, target }) { + await tick(); + assert.htmlEqual(target.innerHTML, 'oops'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-error-in-block-expression/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-error-in-block-expression/main.svelte new file mode 100644 index 0000000000..a49a5c9540 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-error-in-block-expression/main.svelte @@ -0,0 +1,8 @@ + + {#each (await Promise.reject(new Error('oops'))) as x} + hi + {/each} + + {#snippet pending()}loading{/snippet} + {#snippet failed()}oops{/snippet} + diff --git a/packages/svelte/tests/runtime-runes/samples/async-error-recovery/_config.js b/packages/svelte/tests/runtime-runes/samples/async-error-recovery/_config.js new file mode 100644 index 0000000000..1613bf9c61 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-error-recovery/_config.js @@ -0,0 +1,87 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: ` + +

pending...

+ `, + + compileOptions: { + // this tests some behaviour that was broken in dev + dev: true + }, + + async test({ assert, target }) { + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` + +

0

+ ` + ); + + let [button] = target.querySelectorAll('button'); + let [p] = target.querySelectorAll('p'); + + button.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + +

1

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

2

+ ` + ); + + button.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + ` + ); + + const [button1, button2] = target.querySelectorAll('button'); + + button1.click(); + await tick(); + + button2.click(); + await tick(); + + [p] = target.querySelectorAll('p'); + + assert.htmlEqual( + target.innerHTML, + ` + +

4

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

5

+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-error-recovery/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-error-recovery/main.svelte new file mode 100644 index 0000000000..d5246d330e --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-error-recovery/main.svelte @@ -0,0 +1,24 @@ + + + + + +

{await process(count)}

+ + {#snippet pending()} +

pending...

+ {/snippet} + + {#snippet failed(error, reset)} + + {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-error/_config.js b/packages/svelte/tests/runtime-runes/samples/async-error/_config.js new file mode 100644 index 0000000000..dfbd238eeb --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-error/_config.js @@ -0,0 +1,34 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: `

pending

`, + + async test({ assert, target }) { + let [button1, button2, button3] = target.querySelectorAll('button'); + + button1.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + '

oops!

' + ); + + button2.click(); + + const reset = /** @type {HTMLButtonElement} */ (target.querySelector('[data-id="reset"]')); + reset.click(); + + assert.htmlEqual( + target.innerHTML, + '

pending

' + ); + + button3.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + '

wheee

' + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-error/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-error/main.svelte new file mode 100644 index 0000000000..9af5bbaa16 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-error/main.svelte @@ -0,0 +1,20 @@ + + + + + + + +

{await deferred.promise}

+ + {#snippet pending()} +

pending

+ {/snippet} + + {#snippet failed(error, reset)} +

{error.message}

+ + {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-expression/_config.js b/packages/svelte/tests/runtime-runes/samples/async-expression/_config.js new file mode 100644 index 0000000000..3a66ea709f --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-expression/_config.js @@ -0,0 +1,56 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: ` + + + +

pending

+ `, + + async test({ assert, target, raf }) { + const [reset, hello, goodbye] = target.querySelectorAll('button'); + + hello.click(); + raf.tick(0); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + +

hello

+ ` + ); + + reset.click(); + raf.tick(0); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + +

hello

+

updating...

+ ` + ); + + goodbye.click(); + await Promise.resolve(); + raf.tick(0); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + +

goodbye

+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-expression/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-expression/main.svelte new file mode 100644 index 0000000000..42536ab02a --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-expression/main.svelte @@ -0,0 +1,19 @@ + + + + + + + +

{await deferred.promise}

+ + {#if $effect.pending()} +

updating...

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

pending

+ {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-html-tag/_config.js b/packages/svelte/tests/runtime-runes/samples/async-html-tag/_config.js new file mode 100644 index 0000000000..f5b1f3d2c4 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-html-tag/_config.js @@ -0,0 +1,51 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: ` + + + +

pending

+ `, + + async test({ assert, target }) { + const [reset, hello, goodbye] = target.querySelectorAll('button'); + + hello.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + +

hello

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

hello

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

goodbye

+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-html-tag/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-html-tag/main.svelte new file mode 100644 index 0000000000..980bb16d5c --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-html-tag/main.svelte @@ -0,0 +1,15 @@ + + + + + + + +

{@html await deferred.promise}

+ + {#snippet pending()} +

pending

+ {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-if/_config.js b/packages/svelte/tests/runtime-runes/samples/async-if/_config.js new file mode 100644 index 0000000000..3cd67952c3 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-if/_config.js @@ -0,0 +1,31 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: `

pending

`, + + async test({ assert, target }) { + const [reset, t, f] = target.querySelectorAll('button'); + + t.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + '

yes

' + ); + + reset.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + '

yes

' + ); + + f.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + '

no

' + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-if/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-if/main.svelte new file mode 100644 index 0000000000..21a4cbef97 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-if/main.svelte @@ -0,0 +1,19 @@ + + + + + + + + {#if await deferred.promise} +

yes

+ {:else} +

no

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

pending

+ {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-key/_config.js b/packages/svelte/tests/runtime-runes/samples/async-key/_config.js new file mode 100644 index 0000000000..e7e5db3dd8 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-key/_config.js @@ -0,0 +1,46 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: ` + + + +

pending

+ `, + + async test({ assert, target }) { + const [reset, one, two] = target.querySelectorAll('button'); + + const html = ` + + + +

hello

+ `; + + one.click(); + await tick(); + assert.htmlEqual(target.innerHTML, html); + + const h1 = target.querySelector('h1'); + + reset.click(); + await tick(); + assert.htmlEqual(target.innerHTML, html); + + one.click(); + await tick(); + assert.htmlEqual(target.innerHTML, html); + assert.equal(target.querySelector('h1'), h1); + + reset.click(); + await tick(); + assert.htmlEqual(target.innerHTML, html); + + two.click(); + await tick(); + assert.htmlEqual(target.innerHTML, html); + assert.notEqual(target.querySelector('h1'), h1); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-key/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-key/main.svelte new file mode 100644 index 0000000000..5fbdbd47d0 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-key/main.svelte @@ -0,0 +1,17 @@ + + + + + + + + {#key await deferred.promise} +

hello

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

pending

+ {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-linear-order-different-deriveds/_config.js b/packages/svelte/tests/runtime-runes/samples/async-linear-order-different-deriveds/_config.js new file mode 100644 index 0000000000..cb8e0cfca9 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-linear-order-different-deriveds/_config.js @@ -0,0 +1,35 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: `

loading...

`, + + async test({ assert, target }) { + const [both, a, b] = target.querySelectorAll('button'); + + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` + +

1 * 2 = 2

+

2 * 2 = 4

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

2 * 2 = 4

+

4 * 2 = 8

+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-linear-order-different-deriveds/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-linear-order-different-deriveds/main.svelte new file mode 100644 index 0000000000..432eed976c --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-linear-order-different-deriveds/main.svelte @@ -0,0 +1,17 @@ + + + + + + + +

{a} * 2 = {await (a * 2)}

+

{b} * 2 = {b * 2}

+ + {#snippet pending()} +

loading...

+ {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-linear-order-same-derived/_config.js b/packages/svelte/tests/runtime-runes/samples/async-linear-order-same-derived/_config.js new file mode 100644 index 0000000000..5e522ebdb5 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-linear-order-same-derived/_config.js @@ -0,0 +1,30 @@ +import { flushSync, tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + const [a, b, reset1, reset2, resolve1, resolve2] = target.querySelectorAll('button'); + + resolve1.click(); + await tick(); + + const p = /** @type {HTMLElement} */ (target.querySelector('#test')); + + assert.htmlEqual(p.innerHTML, '1 + 2 = 3'); + + flushSync(() => reset1.click()); + flushSync(() => a.click()); + flushSync(() => reset2.click()); + flushSync(() => b.click()); + + resolve2.click(); + await tick(); + + assert.htmlEqual(p.innerHTML, '1 + 2 = 3'); + + resolve1.click(); + await tick(); + + assert.htmlEqual(p.innerHTML, '2 + 3 = 5'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-linear-order-same-derived/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-linear-order-same-derived/main.svelte new file mode 100644 index 0000000000..cc82db0d75 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-linear-order-same-derived/main.svelte @@ -0,0 +1,31 @@ + + + + + + + + + + + + +

{a} + {b} = {await add(a, b)}

+ + {#snippet pending()} +

loading...

+ {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-nested-derived/Child.svelte b/packages/svelte/tests/runtime-runes/samples/async-nested-derived/Child.svelte new file mode 100644 index 0000000000..546494f4c3 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-nested-derived/Child.svelte @@ -0,0 +1,11 @@ + + +

{indirect}

diff --git a/packages/svelte/tests/runtime-runes/samples/async-nested-derived/_config.js b/packages/svelte/tests/runtime-runes/samples/async-nested-derived/_config.js new file mode 100644 index 0000000000..172b44e6e3 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-nested-derived/_config.js @@ -0,0 +1,14 @@ +import { flushSync, tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + await tick(); + assert.htmlEqual(target.innerHTML, '

0

'); + + const button = target.querySelector('button'); + + flushSync(() => button?.click()); + assert.htmlEqual(target.innerHTML, '

1

'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-nested-derived/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-nested-derived/main.svelte new file mode 100644 index 0000000000..f6b0afe98c --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-nested-derived/main.svelte @@ -0,0 +1,15 @@ + + + + + + + + {#snippet pending()} +

pending

+ {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-prop/Child.svelte b/packages/svelte/tests/runtime-runes/samples/async-prop/Child.svelte new file mode 100644 index 0000000000..85d212b1a8 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-prop/Child.svelte @@ -0,0 +1,5 @@ + + +

{value}

diff --git a/packages/svelte/tests/runtime-runes/samples/async-prop/_config.js b/packages/svelte/tests/runtime-runes/samples/async-prop/_config.js new file mode 100644 index 0000000000..66690c120c --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-prop/_config.js @@ -0,0 +1,51 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: ` + + + +

pending

+ `, + + async test({ assert, target }) { + const [reset, hello, again] = target.querySelectorAll('button'); + + hello.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + +

hello

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

hello

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

hello again

+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-prop/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-prop/main.svelte new file mode 100644 index 0000000000..38388607ce --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-prop/main.svelte @@ -0,0 +1,17 @@ + + + + + + + + + + {#snippet pending()} +

pending

+ {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss/_config.js b/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss/_config.js new file mode 100644 index 0000000000..f8a7cfd479 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss/_config.js @@ -0,0 +1,24 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + compileOptions: { + dev: true + }, + + html: `

pending

`, + + async test({ assert, target, warnings }) { + await tick(); + assert.htmlEqual(target.innerHTML, '

3

3

'); + + assert.equal( + warnings[0], + 'Detected reactivity loss when reading `b`. This happens when state is read in an async function after an earlier `await`' + ); + + assert.equal(warnings[1].name, 'TracedAtError'); + + assert.equal(warnings.length, 2); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss/main.svelte new file mode 100644 index 0000000000..bdb1b095c9 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss/main.svelte @@ -0,0 +1,20 @@ + + + + + + +

{await a_plus_b()}

+

{await a + await b}

+ + {#snippet pending()} +

pending

+ {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-redirect-initial/_config.js b/packages/svelte/tests/runtime-runes/samples/async-redirect-initial/_config.js new file mode 100644 index 0000000000..17bb79af08 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-redirect-initial/_config.js @@ -0,0 +1,51 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + const [a, b, c, ok] = target.querySelectorAll('button'); + + assert.htmlEqual( + target.innerHTML, + ` +

b

+ + + + +

pending...

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

c

+ + + + +

c

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

b

+ + + + +

b

+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-redirect-initial/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-redirect-initial/main.svelte new file mode 100644 index 0000000000..b1bb291e2e --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-redirect-initial/main.svelte @@ -0,0 +1,43 @@ + + +

{route}

+ + + + + + + {#if route === 'a'} +

a

+ {/if} + + {#if route === 'b'} + {#if ok} +

b

+ {:else} + {await goto('c')} + {/if} + {/if} + + {#if route === 'c'} +

c

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

pending...

+ {/snippet} + + {#snippet failed(error, reset)} + + {/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 new file mode 100644 index 0000000000..ebbe642860 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-redirect/_config.js @@ -0,0 +1,52 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + assert.htmlEqual( + target.innerHTML, + ` +

a

+ + + + +

a

+ ` + ); + + const [a, b, c, ok] = target.querySelectorAll('button'); + + b.click(); + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` +

c

+ + + + +

c

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

b

+ + + + +

b

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

{route}

+ + + + + + + {#if route === 'a'} +

a

+ {/if} + + {#if route === 'b'} + {#if ok} +

b

+ {:else} + {await goto('c')} + {/if} + {/if} + + {#if route === 'c'} +

c

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

pending...

+ {/snippet} + + {#snippet failed(error, reset)} + + {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-render-tag/_config.js b/packages/svelte/tests/runtime-runes/samples/async-render-tag/_config.js new file mode 100644 index 0000000000..6f3473f592 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-render-tag/_config.js @@ -0,0 +1,51 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: ` + + + +

pending

+ `, + + async test({ assert, target }) { + const [reset, hello, wheee] = target.querySelectorAll('button'); + + hello.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + +

hello

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

hello

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

wheee

+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-render-tag/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-render-tag/main.svelte new file mode 100644 index 0000000000..b59bc319d8 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-render-tag/main.svelte @@ -0,0 +1,19 @@ + + + + + + +{#snippet hello(message)} +

{message}

+{/snippet} + + + {@render hello(await deferred.promise)} + + {#snippet pending()} +

pending

+ {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-slot/Child.svelte b/packages/svelte/tests/runtime-runes/samples/async-slot/Child.svelte new file mode 100644 index 0000000000..c32f869f63 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-slot/Child.svelte @@ -0,0 +1 @@ + diff --git a/packages/svelte/tests/runtime-runes/samples/async-slot/_config.js b/packages/svelte/tests/runtime-runes/samples/async-slot/_config.js new file mode 100644 index 0000000000..3d54d24259 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-slot/_config.js @@ -0,0 +1,13 @@ +import { tick } from 'svelte'; +import { ok, test } from '../../test'; + +export default test({ + html: ` +

loading...

+ `, + + async test({ assert, target }) { + await tick(); + assert.htmlEqual(target.innerHTML, '

hello

'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-slot/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-slot/main.svelte new file mode 100644 index 0000000000..badd60746d --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-slot/main.svelte @@ -0,0 +1,13 @@ + + + + +

{message}

+
+ + {#snippet pending()} +

loading...

+ {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-stale-derived-2/_config.js b/packages/svelte/tests/runtime-runes/samples/async-stale-derived-2/_config.js new file mode 100644 index 0000000000..8276c5be41 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-stale-derived-2/_config.js @@ -0,0 +1,58 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + skip: true, // TODO this one is tricky + + async test({ assert, target }) { + const [increment, a, b] = target.querySelectorAll('button'); + + a.click(); + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` + + + +

a: 0

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

a: 0

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

b: 0

+ let count = $state(0); + + let a = []; + let b = []; + + function push(deferreds, value) { + const deferred = Promise.withResolvers(); + deferreds.push({ deferred, value }); + return deferred.promise; + } + + + + + + + + {#if count % 2 === 0} +

a: {await push(a, count)}

+ {:else} +

b: {await push(b, count)}

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

loading...

+ {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-stale-derived/_config.js b/packages/svelte/tests/runtime-runes/samples/async-stale-derived/_config.js new file mode 100644 index 0000000000..884b27d865 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-stale-derived/_config.js @@ -0,0 +1,52 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + await tick(); + + const [increment, shift] = target.querySelectorAll('button'); + + assert.htmlEqual( + target.innerHTML, + ` + + +

0

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

2

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

delayed: 3

+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-stale-derived/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-stale-derived/main.svelte new file mode 100644 index 0000000000..eeefffbee6 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-stale-derived/main.svelte @@ -0,0 +1,26 @@ + + + + + + + {#if count % 2} +

delayed: {await push()}

+ {:else} +

{await count}

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

loading...

+ {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-svelte-element/_config.js b/packages/svelte/tests/runtime-runes/samples/async-svelte-element/_config.js new file mode 100644 index 0000000000..dc25be10c8 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-svelte-element/_config.js @@ -0,0 +1,51 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: ` + + + +

pending

+ `, + + async test({ assert, target }) { + const [reset, h1, h2] = target.querySelectorAll('button'); + + h1.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + +

hello

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

hello

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

hello

+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-svelte-element/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-svelte-element/main.svelte new file mode 100644 index 0000000000..f8165784dc --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-svelte-element/main.svelte @@ -0,0 +1,15 @@ + + + + + + + + hello + + {#snippet pending()} +

pending

+ {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-top-level-in-if/Child.svelte b/packages/svelte/tests/runtime-runes/samples/async-top-level-in-if/Child.svelte new file mode 100644 index 0000000000..7ad618f130 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-top-level-in-if/Child.svelte @@ -0,0 +1,7 @@ + + +

{value}

diff --git a/packages/svelte/tests/runtime-runes/samples/async-top-level-in-if/_config.js b/packages/svelte/tests/runtime-runes/samples/async-top-level-in-if/_config.js new file mode 100644 index 0000000000..73c9b50a69 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-top-level-in-if/_config.js @@ -0,0 +1,40 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + const [toggle, hello] = target.querySelectorAll('button'); + + assert.htmlEqual( + target.innerHTML, + ` + + + ` + ); + + toggle.click(); + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` + + + ` + ); + + hello.click(); + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` + + +

condition is true

+

hello

+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-top-level-in-if/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-top-level-in-if/main.svelte new file mode 100644 index 0000000000..d111ce6fe3 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-top-level-in-if/main.svelte @@ -0,0 +1,20 @@ + + + + + + + {#if condition} +

condition is {condition}

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

pending

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

{value}

diff --git a/packages/svelte/tests/runtime-runes/samples/async-top-level/_config.js b/packages/svelte/tests/runtime-runes/samples/async-top-level/_config.js new file mode 100644 index 0000000000..b2200201c6 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-top-level/_config.js @@ -0,0 +1,14 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: `

pending

`, + + async test({ assert, target }) { + const [hello] = target.querySelectorAll('button'); + + hello.click(); + await tick(); + assert.htmlEqual(target.innerHTML, '

hello

'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-top-level/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-top-level/main.svelte new file mode 100644 index 0000000000..78ad3ba04a --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-top-level/main.svelte @@ -0,0 +1,15 @@ + + + + + + + + {#snippet pending()} +

pending

+ {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-unresolved-promise/_config.js b/packages/svelte/tests/runtime-runes/samples/async-unresolved-promise/_config.js new file mode 100644 index 0000000000..e9ccbba2b6 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-unresolved-promise/_config.js @@ -0,0 +1,32 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + await tick(); + + const [increment] = target.querySelectorAll('button'); + + assert.htmlEqual( + target.innerHTML, + ` + +

0

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

2

+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-unresolved-promise/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-unresolved-promise/main.svelte new file mode 100644 index 0000000000..e0619a1fe4 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-unresolved-promise/main.svelte @@ -0,0 +1,19 @@ + + + + + + {#if count % 2} +

{await new Promise(() => {})}

+ {:else} +

{await count}

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

loading...

+ {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-waterfall-on-init/_config.js b/packages/svelte/tests/runtime-runes/samples/async-waterfall-on-init/_config.js new file mode 100644 index 0000000000..e2c8b851c1 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-waterfall-on-init/_config.js @@ -0,0 +1,42 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: ` + + +
+

pending

+ `, + + async test({ assert, target }) { + const [button1, button2] = target.querySelectorAll('button'); + + button1.click(); + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` + + +
+

pending

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

true

+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-waterfall-on-init/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-waterfall-on-init/main.svelte new file mode 100644 index 0000000000..86af9bb07e --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-waterfall-on-init/main.svelte @@ -0,0 +1,22 @@ + + + + + +
+ + + {#if await d1.promise} + +

{await d2.promise}

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

pending

+ {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-with-sync-derived/_config.js b/packages/svelte/tests/runtime-runes/samples/async-with-sync-derived/_config.js new file mode 100644 index 0000000000..837dd976e2 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-with-sync-derived/_config.js @@ -0,0 +1,53 @@ +import { flushSync, tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: `

loading...

`, + + async test({ assert, target }) { + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` + + + +

1

+

1

+

1

+ ` + ); + + const [log, x, other] = target.querySelectorAll('button'); + + flushSync(() => x.click()); + flushSync(() => other.click()); + + assert.htmlEqual( + target.innerHTML, + ` + + + +

1

+

1

+

1

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

2

+

2

+

2

+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-with-sync-derived/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-with-sync-derived/main.svelte new file mode 100644 index 0000000000..764007e082 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-with-sync-derived/main.svelte @@ -0,0 +1,19 @@ + + + + + + + +

{x}

+

{await x}

+

{y}

+ + {#snippet pending()} +

loading...

+ {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/effect-cleanup/_config.js b/packages/svelte/tests/runtime-runes/samples/effect-cleanup/_config.js index e55733c148..416f61d23a 100644 --- a/packages/svelte/tests/runtime-runes/samples/effect-cleanup/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/effect-cleanup/_config.js @@ -1,3 +1,4 @@ +import { async_mode } from '../../../helpers'; import { test } from '../../test'; import { flushSync } from 'svelte'; @@ -10,6 +11,12 @@ export default test({ flushSync(() => { b1.click(); }); - assert.deepEqual(logs, ['init 0']); + + // With async mode (which is on by default for runtime-runes) this works as expected, without it + // it works differently: https://github.com/sveltejs/svelte/pull/15564 + assert.deepEqual( + logs, + async_mode ? ['init 0', 'cleanup 0', null, 'init 2', 'cleanup 2', null, 'init 4'] : ['init 0'] + ); } }); diff --git a/packages/svelte/tests/runtime-runes/samples/effect-cleanup/main.svelte b/packages/svelte/tests/runtime-runes/samples/effect-cleanup/main.svelte index 2cdcfdfb58..da38374f82 100644 --- a/packages/svelte/tests/runtime-runes/samples/effect-cleanup/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/effect-cleanup/main.svelte @@ -14,4 +14,4 @@ }) - + diff --git a/packages/svelte/tests/runtime-runes/samples/error-boundary-18/_config.js b/packages/svelte/tests/runtime-runes/samples/error-boundary-18/_config.js index e092d0e7c7..f34668ec45 100644 --- a/packages/svelte/tests/runtime-runes/samples/error-boundary-18/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/error-boundary-18/_config.js @@ -2,6 +2,8 @@ import { flushSync } from 'svelte'; import { test } from '../../test'; export default test({ + skip: true, // TODO unskip once tagged values are in and we can fix this properly + test({ assert, target }) { let btn = target.querySelector('button'); diff --git a/packages/svelte/tests/runtime-runes/samples/set-context-after-await/Child.svelte b/packages/svelte/tests/runtime-runes/samples/set-context-after-await/Child.svelte new file mode 100644 index 0000000000..122a316726 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/set-context-after-await/Child.svelte @@ -0,0 +1,11 @@ + diff --git a/packages/svelte/tests/runtime-runes/samples/set-context-after-await/_config.js b/packages/svelte/tests/runtime-runes/samples/set-context-after-await/_config.js new file mode 100644 index 0000000000..1bf7e71176 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/set-context-after-await/_config.js @@ -0,0 +1,12 @@ +import { test } from '../../test'; + +export default test({ + skip_no_async: true, + async test({ assert, logs }) { + await Promise.resolve(); + await Promise.resolve(); + await Promise.resolve(); + + assert.ok(logs[0].startsWith('set_context_after_init')); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/set-context-after-await/main.svelte b/packages/svelte/tests/runtime-runes/samples/set-context-after-await/main.svelte new file mode 100644 index 0000000000..65d0e623cf --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/set-context-after-await/main.svelte @@ -0,0 +1,11 @@ + + + + + + {#snippet pending()} + ... + {/snippet} + diff --git a/packages/svelte/tests/runtime-runes/samples/set-context-after-mount/_config.js b/packages/svelte/tests/runtime-runes/samples/set-context-after-mount/_config.js new file mode 100644 index 0000000000..4569f42a73 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/set-context-after-mount/_config.js @@ -0,0 +1,16 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; +import { async_mode } from '../../../helpers'; + +export default test({ + async test({ target, assert, logs }) { + const button = target.querySelector('button'); + + flushSync(() => button?.click()); + assert.ok( + async_mode + ? logs[0].startsWith('set_context_after_init') + : logs[0] === 'works without experimental async but really shouldnt' + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/set-context-after-mount/main.svelte b/packages/svelte/tests/runtime-runes/samples/set-context-after-mount/main.svelte new file mode 100644 index 0000000000..0c3b6c3a0f --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/set-context-after-mount/main.svelte @@ -0,0 +1,20 @@ + + + diff --git a/packages/svelte/tests/runtime-runes/samples/tick-timing/_config.js b/packages/svelte/tests/runtime-runes/samples/tick-timing/_config.js index 339cec55c5..25414d4b47 100644 --- a/packages/svelte/tests/runtime-runes/samples/tick-timing/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/tick-timing/_config.js @@ -3,6 +3,8 @@ import { test, ok } from '../../test'; // Tests that tick only resolves after all pending effects have been cleared export default test({ + skip: true, // weirdly, this works if you run it by itself + async test({ assert, target }) { const btn = target.querySelector('button'); ok(btn); diff --git a/packages/svelte/tests/runtime-runes/samples/untrack-own-deriveds/_config.js b/packages/svelte/tests/runtime-runes/samples/untrack-own-deriveds/_config.js index 18062b86fb..b728c3c0be 100644 --- a/packages/svelte/tests/runtime-runes/samples/untrack-own-deriveds/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/untrack-own-deriveds/_config.js @@ -2,6 +2,8 @@ import { flushSync } from 'svelte'; import { test } from '../../test'; export default test({ + // In async mode we _do_ want to run effects that react to their own state changing + skip_async: true, test({ assert, target, logs }) { const button = target.querySelector('button'); diff --git a/packages/svelte/tests/signals/test.ts b/packages/svelte/tests/signals/test.ts index 719c936df0..937324727b 100644 --- a/packages/svelte/tests/signals/test.ts +++ b/packages/svelte/tests/signals/test.ts @@ -6,16 +6,18 @@ import { effect, effect_root, render_effect, - user_effect + user_effect, + user_pre_effect } from '../../src/internal/client/reactivity/effects'; import { state, set, update, update_pre } from '../../src/internal/client/reactivity/sources'; -import type { Derived, Effect, Value } from '../../src/internal/client/types'; +import type { Derived, Effect, Source, Value } from '../../src/internal/client/types'; import { proxy } from '../../src/internal/client/proxy'; import { derived } from '../../src/internal/client/reactivity/deriveds'; import { snapshot } from '../../src/internal/shared/clone.js'; import { SvelteSet } from '../../src/reactivity/set'; import { DESTROYED } from '../../src/internal/client/constants'; import { noop } from 'svelte/internal/client'; +import { disable_async_mode_flag, enable_async_mode_flag } from '../../src/internal/flags'; /** * @param runes runes mode @@ -557,7 +559,7 @@ describe('signals', () => { }; }); - test('schedules rerun when writing to signal before reading it', (runes) => { + test.skip('schedules rerun when writing to signal before reading it', (runes) => { if (!runes) return () => {}; const error = console.error; @@ -1049,31 +1051,85 @@ describe('signals', () => { }; }); - test('effects do not depend on state they own', () => { + test('effects do depend on state they own', (runes) => { + // This behavior is important for use cases like a Resource class + // which shares its instance between multiple effects and triggers + // rerenders by self-invalidating its state. + const log: number[] = []; + + let count: any; + + if (runes) { + // We will make this the new default behavior once it's stable but until then + // we need to keep the old behavior to not break existing code. + enable_async_mode_flag(); + } + + effect(() => { + if (!count || $.get(count) < 2) { + count ||= state(0); + log.push($.get(count)); + set(count, $.get(count) + 1); + } + }); + + return () => { + try { + flushSync(); + if (runes) { + assert.deepEqual(log, [0, 1]); + } else { + assert.deepEqual(log, [0]); + } + } finally { + disable_async_mode_flag(); + } + }; + }); + + test('nested effects depend on state of upper effects', () => { + const logs: number[] = []; + let raw: Source; + let proxied: { current: number }; + user_effect(() => { - const value = state(0); - set(value, $.get(value) + 1); + raw = state(0); + proxied = proxy({ current: 0 }); + + // We need those separate, else one working and rerunning the effect + // could mask the other one not rerunning + user_effect(() => { + logs.push($.get(raw)); + }); + + user_effect(() => { + logs.push(proxied.current); + }); }); return () => { flushSync(); + set(raw, $.get(raw) + 1); + proxied.current += 1; + flushSync(); + assert.deepEqual(logs, [0, 0, 1, 1]); }; }); test('nested effects depend on state of upper effects', () => { const logs: number[] = []; - user_effect(() => { + user_pre_effect(() => { const raw = state(0); const proxied = proxy({ current: 0 }); // We need those separate, else one working and rerunning the effect // could mask the other one not rerunning - user_effect(() => { + user_pre_effect(() => { logs.push($.get(raw)); }); - user_effect(() => { + user_pre_effect(() => { logs.push(proxied.current); }); @@ -1081,7 +1137,7 @@ describe('signals', () => { // together with the reading effects flushSync(); - user_effect(() => { + user_pre_effect(() => { $.untrack(() => { set(raw, $.get(raw) + 1); proxied.current += 1; diff --git a/packages/svelte/tests/store/test.ts b/packages/svelte/tests/store/test.ts index 77cecca7e5..ecb22c1be6 100644 --- a/packages/svelte/tests/store/test.ts +++ b/packages/svelte/tests/store/test.ts @@ -11,6 +11,7 @@ import { } from 'svelte/store'; import { source, set } from '../../src/internal/client/reactivity/sources'; import * as $ from '../../src/internal/client/runtime'; +import { flushSync } from '../../src/internal/client/reactivity/batch'; import { effect_root, render_effect } from 'svelte/internal/client'; describe('writable', () => { @@ -602,7 +603,7 @@ describe('toStore', () => { assert.deepEqual(log, [0]); set(count, 1); - $.flushSync(); + flushSync(); assert.deepEqual(log, [0, 1]); unsubscribe(); @@ -625,7 +626,7 @@ describe('toStore', () => { assert.deepEqual(log, [0]); set(count, 1); - $.flushSync(); + flushSync(); assert.deepEqual(log, [0, 1]); store.set(2); @@ -654,11 +655,11 @@ describe('fromStore', () => { assert.deepEqual(log, [0]); store.set(1); - $.flushSync(); + flushSync(); assert.deepEqual(log, [0, 1]); count.current = 2; - $.flushSync(); + flushSync(); assert.deepEqual(log, [0, 1, 2]); assert.equal(get(store), 2); diff --git a/packages/svelte/tests/suite.ts b/packages/svelte/tests/suite.ts index 0ae06e727f..6954b8b683 100644 --- a/packages/svelte/tests/suite.ts +++ b/packages/svelte/tests/suite.ts @@ -35,7 +35,7 @@ export function suite(fn: (config: Test, test_dir: string export function suite_with_variants( variants: Variants[], - should_skip_variant: (variant: Variants, config: Test) => boolean | 'no-test', + should_skip_variant: (variant: Variants, config: Test, test_name: string) => boolean | 'no-test', common_setup: (config: Test, test_dir: string) => Promise | Common, fn: (config: Test, test_dir: string, variant: Variants, common: Common) => void ) { @@ -46,11 +46,11 @@ export function suite_with_variants void): void; + /** + * Synchronously flush any pending updates. + * Returns void if no callback is provided, otherwise returns the result of calling the callback. + * */ + export function flushSync(fn?: (() => T) | undefined): T; /** * Create a snippet programmatically * */ @@ -443,29 +448,6 @@ declare module 'svelte' { }): Snippet; /** Anything except a function */ type NotFunction = T extends Function ? never : T; - /** - * Synchronously flush any pending updates. - * Returns void if no callback is provided, otherwise returns the result of calling the callback. - * */ - export function flushSync(fn?: (() => T) | undefined): T; - /** - * Returns a promise that resolves once any pending state changes have been applied. - * */ - export function tick(): Promise; - /** - * When used inside a [`$derived`](https://svelte.dev/docs/svelte/$derived) or [`$effect`](https://svelte.dev/docs/svelte/$effect), - * any state read inside `fn` will not be treated as a dependency. - * - * ```ts - * $effect(() => { - * // this will run when `data` changes, but not when `time` changes - * save(data, { - * timestamp: untrack(() => time) - * }); - * }); - * ``` - * */ - export function untrack(fn: () => T): T; /** * Retrieves the context that belongs to the closest parent component with the specified `key`. * Must be called during component initialisation. @@ -539,6 +521,29 @@ declare module 'svelte' { export function unmount(component: Record, options?: { outro?: boolean; } | undefined): Promise; + /** + * Returns a promise that resolves once any pending state changes have been applied. + * */ + export function tick(): Promise; + /** + * Returns a promise that resolves once any state changes, and asynchronous work resulting from them, + * have resolved and the DOM has been updated + * */ + export function settled(): Promise; + /** + * When used inside a [`$derived`](https://svelte.dev/docs/svelte/$derived) or [`$effect`](https://svelte.dev/docs/svelte/$effect), + * any state read inside `fn` will not be treated as a dependency. + * + * ```ts + * $effect(() => { + * // this will run when `data` changes, but not when `time` changes + * save(data, { + * timestamp: untrack(() => time) + * }); + * }); + * ``` + * */ + export function untrack(fn: () => T): T; type Getters = { [K in keyof T]: () => T[K]; }; @@ -1111,6 +1116,11 @@ declare module 'svelte/compiler' { * Use this to filter out warnings. Return `true` to keep the warning, `false` to discard it. */ warningFilter?: (warning: Warning) => boolean; + /** Experimental options */ + experimental?: { + /** Allow `await` keyword in deriveds, template expressions, and the top level of components */ + async?: boolean; + }; } /** * - `html` — the default, for e.g. `
` or `` @@ -3021,6 +3031,11 @@ declare module 'svelte/types/compiler/interfaces' { * Use this to filter out warnings. Return `true` to keep the warning, `false` to discard it. */ warningFilter?: (warning: Warning_1) => boolean; + /** Experimental options */ + experimental?: { + /** Allow `await` keyword in deriveds, template expressions, and the top level of components */ + async?: boolean; + }; } /** * - `html` — the default, for e.g. `
` or `` diff --git a/playgrounds/sandbox/index.html b/playgrounds/sandbox/index.html index 845538abf0..d70409ffb6 100644 --- a/playgrounds/sandbox/index.html +++ b/playgrounds/sandbox/index.html @@ -14,6 +14,12 @@ import { mount, hydrate, unmount } from 'svelte'; import App from '/src/App.svelte'; + globalThis.delayed = (v, ms = 1000) => { + return new Promise((f) => { + setTimeout(() => f(v), ms); + }); + }; + const root = document.getElementById('root'); const render = root.firstChild?.nextSibling ? hydrate : mount; diff --git a/playgrounds/sandbox/run.js b/playgrounds/sandbox/run.js index 2029937f52..639b755020 100644 --- a/playgrounds/sandbox/run.js +++ b/playgrounds/sandbox/run.js @@ -76,7 +76,10 @@ for (const generate of /** @type {const} */ (['client', 'server'])) { dev: false, filename: input, generate, - runes: argv.values.runes + runes: argv.values.runes, + experimental: { + async: true + } }); for (const warning of compiled.warnings) { @@ -94,7 +97,10 @@ for (const generate of /** @type {const} */ (['client', 'server'])) { filename: input, generate, runes: argv.values.runes, - fragments: 'tree' + fragments: 'tree', + experimental: { + async: true + } }); const output_js = `${cwd}/output/${generate}/${file}.tree.js`; @@ -116,7 +122,10 @@ for (const generate of /** @type {const} */ (['client', 'server'])) { const compiled = compileModule(source, { dev: false, filename: input, - generate + generate, + experimental: { + async: true + } }); const output_js = `${cwd}/output/${generate}/${file}`; diff --git a/playgrounds/sandbox/ssr-common.js b/playgrounds/sandbox/ssr-common.js new file mode 100644 index 0000000000..db3e085508 --- /dev/null +++ b/playgrounds/sandbox/ssr-common.js @@ -0,0 +1,17 @@ +Promise.withResolvers ??= () => { + let resolve; + let reject; + + const promise = new Promise((f, r) => { + resolve = f; + reject = r; + }); + + return { promise, resolve, reject }; +}; + +globalThis.delayed = (v, ms = 1000) => { + return new Promise((f) => { + setTimeout(() => f(v), ms); + }); +}; diff --git a/playgrounds/sandbox/ssr-dev.js b/playgrounds/sandbox/ssr-dev.js index 01ce14e266..e019b234a6 100644 --- a/playgrounds/sandbox/ssr-dev.js +++ b/playgrounds/sandbox/ssr-dev.js @@ -5,6 +5,7 @@ import { fileURLToPath } from 'node:url'; import polka from 'polka'; import { createServer as createViteServer } from 'vite'; import { render } from 'svelte/server'; +import './ssr-common.js'; const PORT = process.env.PORT || '5173'; diff --git a/playgrounds/sandbox/ssr-prod.js b/playgrounds/sandbox/ssr-prod.js index 1ed9435249..e8f74ee93a 100644 --- a/playgrounds/sandbox/ssr-prod.js +++ b/playgrounds/sandbox/ssr-prod.js @@ -3,6 +3,7 @@ import path from 'node:path'; import polka from 'polka'; import { render } from 'svelte/server'; import App from './src/App.svelte'; +import './ssr-common.js'; const { head, body } = render(App); diff --git a/playgrounds/sandbox/svelte.config.js b/playgrounds/sandbox/svelte.config.js index 65f739bdb6..68ac605385 100644 --- a/playgrounds/sandbox/svelte.config.js +++ b/playgrounds/sandbox/svelte.config.js @@ -1,5 +1,9 @@ export default { compilerOptions: { - hmr: true + hmr: false, + + experimental: { + async: true + } } };