From 91486fa807c85193b5a52f7558acd9997d96892e Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Thu, 20 Nov 2025 17:23:02 +0100 Subject: [PATCH] fix: take async into account for bindings/transitions/animations/attachments (#17198) * fix: take async into account for bindings/transitions/animations/attachments - block on async work - error at compile time on await expressions. Right now it gives confusing errors later at compile time or at runtime Fixes #17194 * this was weird --- .changeset/itchy-forks-greet.md | 5 ++++ .changeset/plain-bikes-smile.md | 5 ++++ .../98-reference/.generated/compile-errors.md | 6 +++++ .../messages/compile-errors/template.md | 4 +++ packages/svelte/src/compiler/errors.js | 9 +++++++ .../compiler/phases/1-parse/state/element.js | 5 ++-- .../src/compiler/phases/2-analyze/index.js | 2 ++ .../2-analyze/visitors/AnimateDirective.js | 15 +++++++++++ .../phases/2-analyze/visitors/AttachTag.js | 6 ++++- .../2-analyze/visitors/BindDirective.js | 10 +++++++ .../2-analyze/visitors/TransitionDirective.js | 7 ++++- .../phases/2-analyze/visitors/UseDirective.js | 8 +++++- .../client/visitors/AnimateDirective.js | 26 +++++++++++++------ .../3-transform/client/visitors/AttachTag.js | 14 +++++++++- .../client/visitors/TransitionDirective.js | 14 +++++++++- .../client/visitors/UseDirective.js | 14 +++++++++- .../client/visitors/shared/component.js | 3 +++ .../server/visitors/shared/component.js | 4 +++ .../svelte/src/compiler/types/template.d.ts | 12 +++++++++ .../client/dom/elements/transitions.js | 8 +----- .../src/internal/client/reactivity/async.js | 7 ++++- .../samples/async-action-blockers/_config.js | 11 ++++++++ .../samples/async-action-blockers/main.svelte | 13 ++++++++++ .../async-attach-blockers/Child.svelte | 5 ++++ .../samples/async-attach-blockers/_config.js | 11 ++++++++ .../samples/async-attach-blockers/main.svelte | 17 ++++++++++++ .../async-transition-blockers/_config.js | 11 ++++++++ .../async-transition-blockers/main.svelte | 13 ++++++++++ 28 files changed, 240 insertions(+), 25 deletions(-) create mode 100644 .changeset/itchy-forks-greet.md create mode 100644 .changeset/plain-bikes-smile.md create mode 100644 packages/svelte/src/compiler/phases/2-analyze/visitors/AnimateDirective.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-action-blockers/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-action-blockers/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-attach-blockers/Child.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-attach-blockers/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-attach-blockers/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-transition-blockers/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-transition-blockers/main.svelte diff --git a/.changeset/itchy-forks-greet.md b/.changeset/itchy-forks-greet.md new file mode 100644 index 0000000000..9eaf3b9852 --- /dev/null +++ b/.changeset/itchy-forks-greet.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: error at compile time instead of at runtime on await expressions inside bindings/transitions/animations/attachments diff --git a/.changeset/plain-bikes-smile.md b/.changeset/plain-bikes-smile.md new file mode 100644 index 0000000000..19263dd616 --- /dev/null +++ b/.changeset/plain-bikes-smile.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: take async blockers into account for bindings/transitions/animations/attachments diff --git a/documentation/docs/98-reference/.generated/compile-errors.md b/documentation/docs/98-reference/.generated/compile-errors.md index c5703c636b..94ea46b68a 100644 --- a/documentation/docs/98-reference/.generated/compile-errors.md +++ b/documentation/docs/98-reference/.generated/compile-errors.md @@ -561,6 +561,12 @@ Cannot use `await` in deriveds and template expressions, or at the top level of `$host()` can only be used inside custom element component instances ``` +### illegal_await_expression + +``` +`use:`, `transition:` and `animate:` directives, attachments and bindings do not support await expressions +``` + ### illegal_element_attribute ``` diff --git a/packages/svelte/messages/compile-errors/template.md b/packages/svelte/messages/compile-errors/template.md index ac95bfe4a7..dcec3867ef 100644 --- a/packages/svelte/messages/compile-errors/template.md +++ b/packages/svelte/messages/compile-errors/template.md @@ -231,6 +231,10 @@ The same applies to components: > Expected whitespace +## illegal_await_expression + +> `use:`, `transition:` and `animate:` directives, attachments and bindings do not support await expressions + ## illegal_element_attribute > `<%name%>` does not support non-event attributes or spread attributes diff --git a/packages/svelte/src/compiler/errors.js b/packages/svelte/src/compiler/errors.js index 5e3968215f..25304e48c8 100644 --- a/packages/svelte/src/compiler/errors.js +++ b/packages/svelte/src/compiler/errors.js @@ -1148,6 +1148,15 @@ export function expected_whitespace(node) { e(node, 'expected_whitespace', `Expected whitespace\nhttps://svelte.dev/e/expected_whitespace`); } +/** + * `use:`, `transition:` and `animate:` directives, attachments and bindings do not support await expressions + * @param {null | number | NodeLike} node + * @returns {never} + */ +export function illegal_await_expression(node) { + e(node, 'illegal_await_expression', `\`use:\`, \`transition:\` and \`animate:\` directives, attachments and bindings do not support await expressions\nhttps://svelte.dev/e/illegal_await_expression`); +} + /** * `<%name%>` does not support non-event attributes or spread attributes * @param {null | number | NodeLike} node diff --git a/packages/svelte/src/compiler/phases/1-parse/state/element.js b/packages/svelte/src/compiler/phases/1-parse/state/element.js index 9167888e37..bd1bd33c41 100644 --- a/packages/svelte/src/compiler/phases/1-parse/state/element.js +++ b/packages/svelte/src/compiler/phases/1-parse/state/element.js @@ -654,8 +654,7 @@ function read_attribute(parser) { } } - /** @type {AST.Directive} */ - const directive = { + const directive = /** @type {AST.Directive} */ ({ start, end, type, @@ -664,7 +663,7 @@ function read_attribute(parser) { metadata: { expression: new ExpressionMetadata() } - }; + }); // @ts-expect-error we do this separately from the declaration to avoid upsetting typescript directive.modifiers = modifiers; diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index 4206f3df9a..fb026fa78f 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -24,6 +24,7 @@ import { extract_svelte_ignore } from '../../utils/extract_svelte_ignore.js'; import { ignore_map, ignore_stack, pop_ignore, push_ignore } from '../../state.js'; import { ArrowFunctionExpression } from './visitors/ArrowFunctionExpression.js'; import { AssignmentExpression } from './visitors/AssignmentExpression.js'; +import { AnimateDirective } from './visitors/AnimateDirective.js'; import { AttachTag } from './visitors/AttachTag.js'; import { Attribute } from './visitors/Attribute.js'; import { AwaitBlock } from './visitors/AwaitBlock.js'; @@ -142,6 +143,7 @@ const visitors = { pop_ignore(); } }, + AnimateDirective, ArrowFunctionExpression, AssignmentExpression, AttachTag, diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/AnimateDirective.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/AnimateDirective.js new file mode 100644 index 0000000000..3b4f7007be --- /dev/null +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/AnimateDirective.js @@ -0,0 +1,15 @@ +/** @import { Context } from '../types' */ +/** @import { AST } from '#compiler'; */ +import * as e from '../../../errors.js'; + +/** + * @param {AST.AnimateDirective} node + * @param {Context} context + */ +export function AnimateDirective(node, context) { + context.next({ ...context.state, expression: node.metadata.expression }); + + if (node.metadata.expression.has_await) { + e.illegal_await_expression(node); + } +} diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/AttachTag.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/AttachTag.js index 1e318f228d..f9a8c1b69d 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/AttachTag.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/AttachTag.js @@ -1,7 +1,7 @@ /** @import { AST } from '#compiler' */ /** @import { Context } from '../types' */ - import { mark_subtree_dynamic } from './shared/fragment.js'; +import * as e from '../../../errors.js'; /** * @param {AST.AttachTag} node @@ -10,4 +10,8 @@ import { mark_subtree_dynamic } from './shared/fragment.js'; export function AttachTag(node, context) { mark_subtree_dynamic(context.path); context.next({ ...context.state, expression: node.metadata.expression }); + + if (node.metadata.expression.has_await) { + e.illegal_await_expression(node); + } } diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/BindDirective.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/BindDirective.js index aeae121278..ab541703a0 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/BindDirective.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/BindDirective.js @@ -161,6 +161,7 @@ export function BindDirective(node, context) { const [get, set] = node.expression.expressions; // We gotta jump across the getter/setter functions to avoid the expression metadata field being reset to null + // as we want to collect the functions' blocker/async info context.visit(get.type === 'ArrowFunctionExpression' ? get.body : get, { ...context.state, expression: node.metadata.expression @@ -169,6 +170,11 @@ export function BindDirective(node, context) { ...context.state, expression: node.metadata.expression }); + + if (node.metadata.expression.has_await) { + e.illegal_await_expression(node); + } + return; } @@ -267,4 +273,8 @@ export function BindDirective(node, context) { } context.next({ ...context.state, expression: node.metadata.expression }); + + if (node.metadata.expression.has_await) { + e.illegal_await_expression(node); + } } diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/TransitionDirective.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/TransitionDirective.js index c218f741c3..e50e28a04c 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/TransitionDirective.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/TransitionDirective.js @@ -1,5 +1,6 @@ /** @import { AST } from '#compiler' */ /** @import { Context } from '../types' */ +import * as e from '../../../errors.js'; import { mark_subtree_dynamic } from './shared/fragment.js'; @@ -10,5 +11,9 @@ import { mark_subtree_dynamic } from './shared/fragment.js'; export function TransitionDirective(node, context) { mark_subtree_dynamic(context.path); - context.next(); + context.next({ ...context.state, expression: node.metadata.expression }); + + if (node.metadata.expression.has_await) { + e.illegal_await_expression(node); + } } diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/UseDirective.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/UseDirective.js index 76706ad674..eb340382da 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/UseDirective.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/UseDirective.js @@ -1,6 +1,7 @@ /** @import { AST } from '#compiler' */ /** @import { Context } from '../types' */ import { mark_subtree_dynamic } from './shared/fragment.js'; +import * as e from '../../../errors.js'; /** * @param {AST.UseDirective} node @@ -8,5 +9,10 @@ import { mark_subtree_dynamic } from './shared/fragment.js'; */ export function UseDirective(node, context) { mark_subtree_dynamic(context.path); - context.next(); + + context.next({ ...context.state, expression: node.metadata.expression }); + + if (node.metadata.expression.has_await) { + e.illegal_await_expression(node); + } } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AnimateDirective.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AnimateDirective.js index 16f9735370..c1f1991fa0 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AnimateDirective.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AnimateDirective.js @@ -15,14 +15,24 @@ export function AnimateDirective(node, context) { : b.thunk(/** @type {Expression} */ (context.visit(node.expression))); // in after_update to ensure it always happens after bind:this - context.state.after_update.push( - b.stmt( - b.call( - '$.animation', - context.state.node, - b.thunk(/** @type {Expression} */ (context.visit(parse_directive_name(node.name)))), - expression - ) + let statement = b.stmt( + b.call( + '$.animation', + context.state.node, + b.thunk(/** @type {Expression} */ (context.visit(parse_directive_name(node.name)))), + expression ) ); + + if (node.metadata.expression.is_async()) { + statement = b.stmt( + b.call( + '$.run_after_blockers', + node.metadata.expression.blockers(), + b.thunk(b.block([statement])) + ) + ); + } + + context.state.after_update.push(statement); } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AttachTag.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AttachTag.js index 8b1570c7dc..c594a1a1bd 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AttachTag.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AttachTag.js @@ -9,6 +9,18 @@ import { build_expression } from './shared/utils.js'; */ export function AttachTag(node, context) { const expression = build_expression(context, node.expression, node.metadata.expression); - context.state.init.push(b.stmt(b.call('$.attach', context.state.node, b.thunk(expression)))); + let statement = b.stmt(b.call('$.attach', context.state.node, b.thunk(expression))); + + if (node.metadata.expression.is_async()) { + statement = b.stmt( + b.call( + '$.run_after_blockers', + node.metadata.expression.blockers(), + b.thunk(b.block([statement])) + ) + ); + } + + context.state.init.push(statement); context.next(); } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/TransitionDirective.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/TransitionDirective.js index 41340c1290..bab843de02 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/TransitionDirective.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/TransitionDirective.js @@ -25,5 +25,17 @@ export function TransitionDirective(node, context) { } // in after_update to ensure it always happens after bind:this - context.state.after_update.push(b.stmt(b.call('$.transition', ...args))); + let statement = b.stmt(b.call('$.transition', ...args)); + + if (node.metadata.expression.is_async()) { + statement = b.stmt( + b.call( + '$.run_after_blockers', + node.metadata.expression.blockers(), + b.thunk(b.block([statement])) + ) + ); + } + + context.state.after_update.push(statement); } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/UseDirective.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/UseDirective.js index b95f2fc3ef..13d3057651 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/UseDirective.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/UseDirective.js @@ -32,6 +32,18 @@ export function UseDirective(node, context) { } // actions need to run after attribute updates in order with bindings/events - context.state.init.push(b.stmt(b.call('$.action', ...args))); + let statement = b.stmt(b.call('$.action', ...args)); + + if (node.metadata.expression.is_async()) { + statement = b.stmt( + b.call( + '$.run_after_blockers', + node.metadata.expression.blockers(), + b.thunk(b.block([statement])) + ) + ); + } + + context.state.init.push(statement); context.next(); } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js index b3139a29aa..d70048ad7e 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js @@ -296,6 +296,9 @@ export function build_component(node, component_name, context) { ); } + // TODO also support await expressions here? + memoizer.check_blockers(attribute.metadata.expression); + push_prop(b.prop('init', b.call('$.attachment'), expression, true)); } } diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/component.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/component.js index 6f2ff38bc1..a90b5e41df 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/component.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/component.js @@ -142,6 +142,10 @@ export function build_inline_component(node, expression, context) { true ); } + } else if (attribute.type === 'AttachTag') { + // While we don't run attachments on the server, on the client they might generate a surrounding blocker function which generates + // extra comments, and to prevent hydration mismatches we therefore have to account for them here to generate similar comments on the server. + optimiser.check_blockers(attribute.metadata.expression); } } diff --git a/packages/svelte/src/compiler/types/template.d.ts b/packages/svelte/src/compiler/types/template.d.ts index fd664f107c..1f62994807 100644 --- a/packages/svelte/src/compiler/types/template.d.ts +++ b/packages/svelte/src/compiler/types/template.d.ts @@ -194,6 +194,10 @@ export namespace AST { name: string; /** The y in `animate:x={y}` */ expression: null | Expression; + /** @internal */ + metadata: { + expression: ExpressionMetadata; + }; } /** A `bind:` directive */ @@ -285,6 +289,10 @@ export namespace AST { intro: boolean; /** True if this is a `transition:` or `out:` directive */ outro: boolean; + /** @internal */ + metadata: { + expression: ExpressionMetadata; + }; } /** A `use:` directive */ @@ -294,6 +302,10 @@ export namespace AST { name: string; /** The 'y' in `use:x={y}` */ expression: null | Expression; + /** @internal */ + metadata: { + expression: ExpressionMetadata; + }; } interface BaseElement extends BaseNode { diff --git a/packages/svelte/src/internal/client/dom/elements/transitions.js b/packages/svelte/src/internal/client/dom/elements/transitions.js index 00fad9ffdb..d1d034d402 100644 --- a/packages/svelte/src/internal/client/dom/elements/transitions.js +++ b/packages/svelte/src/internal/client/dom/elements/transitions.js @@ -1,13 +1,7 @@ /** @import { AnimateFn, Animation, AnimationConfig, EachItem, Effect, TransitionFn, TransitionManager } from '#client' */ import { noop, is_function } from '../../../shared/utils.js'; import { effect } from '../../reactivity/effects.js'; -import { - active_effect, - active_reaction, - set_active_effect, - set_active_reaction, - untrack -} from '../../runtime.js'; +import { active_effect, untrack } from '../../runtime.js'; import { loop } from '../../loop.js'; import { should_intro } from '../../render.js'; import { current_each_item } from '../blocks/each.js'; diff --git a/packages/svelte/src/internal/client/reactivity/async.js b/packages/svelte/src/internal/client/reactivity/async.js index e48aff3d7f..50c36ed021 100644 --- a/packages/svelte/src/internal/client/reactivity/async.js +++ b/packages/svelte/src/internal/client/reactivity/async.js @@ -26,6 +26,7 @@ import { } from './deriveds.js'; import { aborted } from './effects.js'; import { hydrate_next, hydrating, set_hydrate_node, skip_nodes } from '../dom/hydration.js'; +import { current_each_item, set_current_each_item } from '../dom/blocks/each.js'; /** * @param {Array>} blockers @@ -89,7 +90,11 @@ export function flatten(blockers, sync, async, fn) { * @param {(values: Value[]) => any} fn */ export function run_after_blockers(blockers, fn) { - flatten(blockers, [], [], fn); + var each_item = current_each_item; // TODO should this be part of capture? + flatten(blockers, [], [], (v) => { + set_current_each_item(each_item); + fn(v); + }); } /** diff --git a/packages/svelte/tests/runtime-runes/samples/async-action-blockers/_config.js b/packages/svelte/tests/runtime-runes/samples/async-action-blockers/_config.js new file mode 100644 index 0000000000..9d8aea3c17 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-action-blockers/_config.js @@ -0,0 +1,11 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + mode: ['client', 'hydrate'], + async test({ assert, logs }) { + await tick(); + + assert.deepEqual(logs, ['ready']); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-action-blockers/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-action-blockers/main.svelte new file mode 100644 index 0000000000..c9a1477a00 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-action-blockers/main.svelte @@ -0,0 +1,13 @@ + + +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-attach-blockers/Child.svelte b/packages/svelte/tests/runtime-runes/samples/async-attach-blockers/Child.svelte new file mode 100644 index 0000000000..346f9c4a19 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-attach-blockers/Child.svelte @@ -0,0 +1,5 @@ + + +
\ No newline at end of file diff --git a/packages/svelte/tests/runtime-runes/samples/async-attach-blockers/_config.js b/packages/svelte/tests/runtime-runes/samples/async-attach-blockers/_config.js new file mode 100644 index 0000000000..4c83634e70 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-attach-blockers/_config.js @@ -0,0 +1,11 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + mode: ['client', 'hydrate'], + async test({ assert, logs }) { + await tick(); + + assert.deepEqual(logs, ['ready', 'ready']); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-attach-blockers/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-attach-blockers/main.svelte new file mode 100644 index 0000000000..30ba602350 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-attach-blockers/main.svelte @@ -0,0 +1,17 @@ + + +
+ diff --git a/packages/svelte/tests/runtime-runes/samples/async-transition-blockers/_config.js b/packages/svelte/tests/runtime-runes/samples/async-transition-blockers/_config.js new file mode 100644 index 0000000000..9d8aea3c17 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-transition-blockers/_config.js @@ -0,0 +1,11 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + mode: ['client', 'hydrate'], + async test({ assert, logs }) { + await tick(); + + assert.deepEqual(logs, ['ready']); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-transition-blockers/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-transition-blockers/main.svelte new file mode 100644 index 0000000000..ff5059e129 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-transition-blockers/main.svelte @@ -0,0 +1,13 @@ + + +