diff --git a/.changeset/shaggy-comics-fail.md b/.changeset/shaggy-comics-fail.md
deleted file mode 100644
index 981a25c978..0000000000
--- a/.changeset/shaggy-comics-fail.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'svelte': patch
----
-
-fix: `append_styles` in an effect to make them available on mount
diff --git a/.changeset/shiny-berries-call.md b/.changeset/shiny-berries-call.md
deleted file mode 100644
index adc62b3cd0..0000000000
--- a/.changeset/shiny-berries-call.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'svelte': patch
----
-
-chore: remove `parser.template_untrimmed`
diff --git a/.changeset/wise-hairs-pay.md b/.changeset/wise-hairs-pay.md
deleted file mode 100644
index 7d96c1daab..0000000000
--- a/.changeset/wise-hairs-pay.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'svelte': patch
----
-
-fix: always inject styles when compiling as a custom element
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 046ad335f3..51408fc8cc 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -60,6 +60,23 @@ jobs:
env:
CI: true
SVELTE_NO_ASYNC: true
+ TSGo:
+ permissions: {}
+ runs-on: ubuntu-latest
+ timeout-minutes: 5
+ steps:
+ - uses: actions/checkout@v4
+ - uses: pnpm/action-setup@v4
+ - uses: actions/setup-node@v4
+ with:
+ node-version: 24
+ cache: pnpm
+ - name: install
+ run: pnpm install --frozen-lockfile
+ - name: install tsgo
+ run: cd packages/svelte && pnpm i -D @typescript/native-preview
+ - name: type check
+ run: cd packages/svelte && pnpm check:tsgo
Lint:
permissions: {}
runs-on: ubuntu-latest
diff --git a/documentation/docs/02-runes/04-$effect.md b/documentation/docs/02-runes/04-$effect.md
index 5820e178a0..6c42f55795 100644
--- a/documentation/docs/02-runes/04-$effect.md
+++ b/documentation/docs/02-runes/04-$effect.md
@@ -135,7 +135,7 @@ An effect only reruns when the object it reads changes, not when a property insi
An effect only depends on the values that it read the last time it ran. This has interesting implications for effects that have conditional code.
-For instance, if `condition` is `true` in the code snippet below, the code inside the `if` block will run and `color` will be evaluated. As such, changes to either `condition` or `color` [will cause the effect to re-run](/playground/untitled#H4sIAAAAAAAAE21RQW6DMBD8ytaNBJHaJFLViwNIVZ8RcnBgXVk1xsILTYT4e20TQg89IOPZ2fHM7siMaJBx9tmaWpFqjQNlAKXEihx7YVJpdIyfRkY3G4gB8Pi97cPanRtQU8AuwuF_eNUaQuPlOMtc1SlLRWlKUo1tOwJflUikQHZtA0klzCDc64Imx0ANn8bInV1CDhtHgjClrsftcSXotluLybOUb3g4JJHhOZs5WZpuIS9gjNqkJKQP5e2ClrR4SMdZ13E4xZ8zTPOTJU2A2uE_PQ9COCI926_hTVarIU4hu_REPlBrKq2q73ycrf1N-vS4TMUsulaVg3EtR8H9rFgsg8uUsT1B2F9eshigZHBRpuaD0D3mY8Qm2BfB5N2YyRzdNEYVDy0Ja-WsFjcOUuP1HvFLWA6H3XuHTUSmmDV2--0TXonxsKbp7G9C6R__NONS-MFNvxj_d6mBAgAA).
+For instance, if `condition` is `true` in the code snippet below, the code inside the `if` block will run and `color` will be evaluated. This means that changes to either `condition` or `color` [will cause the effect to re-run](/playground/untitled#H4sIAAAAAAAAE21RQW6DMBD8ytaNBJHaJFLViwNIVZ8RcnBgXVk1xsILTYT4e20TQg89IOPZ2fHM7siMaJBx9tmaWpFqjQNlAKXEihx7YVJpdIyfRkY3G4gB8Pi97cPanRtQU8AuwuF_eNUaQuPlOMtc1SlLRWlKUo1tOwJflUikQHZtA0klzCDc64Imx0ANn8bInV1CDhtHgjClrsftcSXotluLybOUb3g4JJHhOZs5WZpuIS9gjNqkJKQP5e2ClrR4SMdZ13E4xZ8zTPOTJU2A2uE_PQ9COCI926_hTVarIU4hu_REPlBrKq2q73ycrf1N-vS4TMUsulaVg3EtR8H9rFgsg8uUsT1B2F9eshigZHBRpuaD0D3mY8Qm2BfB5N2YyRzdNEYVDy0Ja-WsFjcOUuP1HvFLWA6H3XuHTUSmmDV2--0TXonxsKbp7G9C6R__NONS-MFNvxj_d6mBAgAA).
Conversely, if `condition` is `false`, `color` will not be evaluated, and the effect will _only_ re-run again when `condition` changes.
diff --git a/documentation/docs/03-template-syntax/06-snippet.md b/documentation/docs/03-template-syntax/06-snippet.md
index ab536c6e5c..02f58e0f6c 100644
--- a/documentation/docs/03-template-syntax/06-snippet.md
+++ b/documentation/docs/03-template-syntax/06-snippet.md
@@ -277,4 +277,4 @@ Snippets can be created programmatically with the [`createRawSnippet`](svelte#cr
## Snippets and slots
-In Svelte 4, content can be passed to components using [slots](legacy-slots). Snippets are more powerful and flexible, and as such slots are deprecated in Svelte 5.
+In Svelte 4, content can be passed to components using [slots](legacy-slots). Snippets are more powerful and flexible, and so slots have been deprecated in Svelte 5.
diff --git a/documentation/docs/03-template-syntax/08-@html.md b/documentation/docs/03-template-syntax/08-@html.md
index 30456fa666..6d8a8be0c6 100644
--- a/documentation/docs/03-template-syntax/08-@html.md
+++ b/documentation/docs/03-template-syntax/08-@html.md
@@ -22,7 +22,7 @@ It also will not compile Svelte code.
## Styling
-Content rendered this way is 'invisible' to Svelte and as such will not receive [scoped styles](scoped-styles) — in other words, this will not work, and the `a` and `img` styles will be regarded as unused:
+Content rendered this way is 'invisible' to Svelte and as such will not receive [scoped styles](scoped-styles). In other words, this will not work, and the `a` and `img` styles will be regarded as unused:
```svelte
diff --git a/documentation/docs/03-template-syntax/18-class.md b/documentation/docs/03-template-syntax/18-class.md
index 1ea4a208df..db85db4b37 100644
--- a/documentation/docs/03-template-syntax/18-class.md
+++ b/documentation/docs/03-template-syntax/18-class.md
@@ -71,7 +71,7 @@ The user of this component has the same flexibility to use a mixture of objects,
```
-Svelte also exposes the `ClassValue` type, which is the type of value that the `class` attribute on elements accept. This is useful if you want to use a type-safe class name in component props:
+Since Svelte 5.19, Svelte also exposes the `ClassValue` type, which is the type of value that the `class` attribute on elements accept. This is useful if you want to use a type-safe class name in component props:
```svelte
```
-> [!NOTE] This behaviour will only work when the function passed to `onMount` _synchronously_ returns a value. `async` functions always return a `Promise`, and as such cannot _synchronously_ return a function.
+> [!NOTE] This behaviour will only work when the function passed to `onMount` is _synchronous_. `async` functions always return a `Promise`.
## `onDestroy`
diff --git a/documentation/docs/07-misc/07-v5-migration-guide.md b/documentation/docs/07-misc/07-v5-migration-guide.md
index c24c1febee..37da3b7b23 100644
--- a/documentation/docs/07-misc/07-v5-migration-guide.md
+++ b/documentation/docs/07-misc/07-v5-migration-guide.md
@@ -245,7 +245,7 @@ In Svelte 4, you can add event modifiers to handlers:
```
-Modifiers are specific to `on:` and as such do not work with modern event handlers. Adding things like `event.preventDefault()` inside the handler itself is preferable, since all the logic lives in one place rather than being split between handler and modifiers.
+Modifiers are specific to `on:` and so do not work with modern event handlers. Adding things like `event.preventDefault()` inside the handler itself is preferable, since all the logic lives in one place rather than being split between handler and modifiers.
Since event handlers are just functions, you can create your own wrappers as necessary:
@@ -340,7 +340,7 @@ When spreading props, local event handlers must go _after_ the spread, or they r
## Snippets instead of slots
-In Svelte 4, content can be passed to components using slots. Svelte 5 replaces them with snippets which are more powerful and flexible, and as such slots are deprecated in Svelte 5.
+In Svelte 4, content can be passed to components using slots. Svelte 5 replaces them with snippets, which are more powerful and flexible, and so slots are deprecated in Svelte 5.
They continue to work, however, and you can pass snippets to a component that uses slots:
@@ -599,7 +599,7 @@ Note that `mount` and `hydrate` are _not_ synchronous, so things like `onMount`
### Server API changes
-Similarly, components no longer have a `render` method when compiled for server side rendering. Instead, pass the function to `render` from `svelte/server`:
+Similarly, components no longer have a `render` method when compiled for server-side rendering. Instead, pass the function to `render` from `svelte/server`:
```js
+++import { render } from 'svelte/server';+++
@@ -803,7 +803,7 @@ Note that Svelte 5 will also warn if you have a single expression wrapped in quo
### HTML structure is stricter
-In Svelte 4, you were allowed to write HTML code that would be repaired by the browser when server side rendering it. For example you could write this...
+In Svelte 4, you were allowed to write HTML code that would be repaired by the browser when server-side rendering it. For example you could write this...
```svelte
@@ -835,7 +835,7 @@ Assignments to destructured parts of a `@const` declaration are no longer allowe
### :is(...), :has(...), and :where(...) are scoped
-Previously, Svelte did not analyse selectors inside `:is(...)`, `:has(...)`, and `:where(...)`, effectively treating them as global. Svelte 5 analyses them in the context of the current component. As such, some selectors may now be treated as unused if they were relying on this treatment. To fix this, use `:global(...)` inside the `:is(...)/:has(...)/:where(...)` selectors.
+Previously, Svelte did not analyse selectors inside `:is(...)`, `:has(...)`, and `:where(...)`, effectively treating them as global. Svelte 5 analyses them in the context of the current component. Some selectors may now therefore be treated as unused if they were relying on this treatment. To fix this, use `:global(...)` inside the `:is(...)/:has(...)/:where(...)` selectors.
When using Tailwind's `@apply` directive, add a `:global` selector to preserve rules that use Tailwind-generated `:is(...)` selectors:
@@ -964,7 +964,7 @@ Since these mismatches are extremely rare, Svelte 5 assumes that the values are
### Hydration works differently
-Svelte 5 makes use of comments during server side rendering which are used for more robust and efficient hydration on the client. As such, you shouldn't remove comments from your HTML output if you intend to hydrate it, and if you manually authored HTML to be hydrated by a Svelte component, you need to adjust that HTML to include said comments at the correct positions.
+Svelte 5 makes use of comments during server-side rendering which are used for more robust and efficient hydration on the client. You therefore should not remove comments from your HTML output if you intend to hydrate it, and if you manually authored HTML to be hydrated by a Svelte component, you need to adjust that HTML to include said comments at the correct positions.
### `onevent` attributes are delegated
diff --git a/documentation/docs/98-reference/.generated/compile-warnings.md b/documentation/docs/98-reference/.generated/compile-warnings.md
index 01003f30c5..17841b863c 100644
--- a/documentation/docs/98-reference/.generated/compile-warnings.md
+++ b/documentation/docs/98-reference/.generated/compile-warnings.md
@@ -679,7 +679,7 @@ In HTML, there's [no such thing as a self-closing tag](https://jakearchibald.com
```
-Some templating languages (including Svelte) will 'fix' HTML by turning `` into ``. Others adhere to the spec. Both result in ambiguity and confusion when copy-pasting code between different contexts, and as such Svelte prompts you to resolve the ambiguity directly by having an explicit closing tag.
+Some templating languages (including Svelte) will 'fix' HTML by turning `` into ``. Others adhere to the spec. Both result in ambiguity and confusion when copy-pasting code between different contexts, so Svelte prompts you to resolve the ambiguity directly by having an explicit closing tag.
To automate this, run the dedicated migration:
diff --git a/documentation/docs/98-reference/21-svelte-action.md b/documentation/docs/98-reference/21-svelte-action.md
index 53423ec409..ef3ebfbf70 100644
--- a/documentation/docs/98-reference/21-svelte-action.md
+++ b/documentation/docs/98-reference/21-svelte-action.md
@@ -2,4 +2,6 @@
title: svelte/action
---
+This module provides types for [actions](use), which have been superseded by [attachments](@attach).
+
> MODULE: svelte/action
diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md
index f939f69d28..bb61b50dd5 100644
--- a/packages/svelte/CHANGELOG.md
+++ b/packages/svelte/CHANGELOG.md
@@ -1,5 +1,61 @@
# svelte
+## 5.38.1
+
+### Patch Changes
+
+- fix: wrap `abort` in `without_reactive_context` ([#16570](https://github.com/sveltejs/svelte/pull/16570))
+
+- fix: add `hint` as a possible value for `popover` attribute ([#16581](https://github.com/sveltejs/svelte/pull/16581))
+
+- fix: skip effects inside dynamic component that is about to be destroyed ([#16601](https://github.com/sveltejs/svelte/pull/16601))
+
+## 5.38.0
+
+### Minor Changes
+
+- feat: allow `await` inside `@const` declarations ([#16542](https://github.com/sveltejs/svelte/pull/16542))
+
+### Patch Changes
+
+- fix: remount at any hydration error ([#16248](https://github.com/sveltejs/svelte/pull/16248))
+
+- chore: emit `await_reactivity_loss` in `for await` loops ([#16521](https://github.com/sveltejs/svelte/pull/16521))
+
+- fix: emit `snippet_invalid_export` instead of `undefined_export` for exported snippets ([#16539](https://github.com/sveltejs/svelte/pull/16539))
+
+## 5.37.3
+
+### Patch Changes
+
+- fix: reset attribute cache after setting corresponding property ([#16543](https://github.com/sveltejs/svelte/pull/16543))
+
+## 5.37.2
+
+### Patch Changes
+
+- fix: double event processing in firefox due to event object being garbage collected ([#16527](https://github.com/sveltejs/svelte/pull/16527))
+
+- fix: add bindable dimension attributes types to SVG and MathML elements ([#16525](https://github.com/sveltejs/svelte/pull/16525))
+
+- fix: correctly differentiate static fields before emitting `duplicate_class_field` ([#16526](https://github.com/sveltejs/svelte/pull/16526))
+
+- fix: prevent last_propagated_event from being DCE'd ([#16538](https://github.com/sveltejs/svelte/pull/16538))
+
+## 5.37.1
+
+### Patch Changes
+
+- chore: remove some todos ([#16515](https://github.com/sveltejs/svelte/pull/16515))
+
+- fix: allow await expressions inside `{#await ...}` argument ([#16514](https://github.com/sveltejs/svelte/pull/16514))
+
+- fix: `append_styles` in an effect to make them available on mount ([#16509](https://github.com/sveltejs/svelte/pull/16509))
+
+- chore: remove `parser.template_untrimmed` ([#16511](https://github.com/sveltejs/svelte/pull/16511))
+
+- fix: always inject styles when compiling as a custom element ([#16509](https://github.com/sveltejs/svelte/pull/16509))
+
## 5.37.0
### Minor Changes
diff --git a/packages/svelte/README.md b/packages/svelte/README.md
index 03e4db5db0..026bc0d81d 100644
--- a/packages/svelte/README.md
+++ b/packages/svelte/README.md
@@ -30,7 +30,7 @@ See [the SvelteKit documentation](https://svelte.dev/docs/kit) to learn more.
## Changelog
-[The Changelog for this package is available on GitHub](https://github.com/sveltejs/svelte/blob/master/packages/svelte/CHANGELOG.md).
+[The Changelog for this package is available on GitHub](https://github.com/sveltejs/svelte/blob/main/packages/svelte/CHANGELOG.md).
## Supporting Svelte
diff --git a/packages/svelte/elements.d.ts b/packages/svelte/elements.d.ts
index 2e1042dfd6..f63a31a96b 100644
--- a/packages/svelte/elements.d.ts
+++ b/packages/svelte/elements.d.ts
@@ -464,6 +464,14 @@ export interface DOMAttributes {
onfullscreenerror?: EventHandler | undefined | null;
onfullscreenerrorcapture?: EventHandler | undefined | null;
+ // Dimensions
+ readonly 'bind:contentRect'?: DOMRectReadOnly | undefined | null;
+ readonly 'bind:contentBoxSize'?: Array | undefined | null;
+ readonly 'bind:borderBoxSize'?: Array | undefined | null;
+ readonly 'bind:devicePixelContentBoxSize'?: Array | undefined | null;
+ readonly 'bind:clientWidth'?: number | undefined | null;
+ readonly 'bind:clientHeight'?: number | undefined | null;
+
xmlns?: string | undefined | null;
}
@@ -773,7 +781,7 @@ export interface HTMLAttributes extends AriaAttributes, D
title?: string | undefined | null;
translate?: 'yes' | 'no' | '' | undefined | null;
inert?: boolean | undefined | null;
- popover?: 'auto' | 'manual' | '' | undefined | null;
+ popover?: 'auto' | 'manual' | 'hint' | '' | undefined | null;
writingsuggestions?: Booleanish | undefined | null;
// Unknown
@@ -839,13 +847,7 @@ export interface HTMLAttributes extends AriaAttributes, D
*/
'bind:innerText'?: string | undefined | null;
- readonly 'bind:contentRect'?: DOMRectReadOnly | undefined | null;
- readonly 'bind:contentBoxSize'?: Array | undefined | null;
- readonly 'bind:borderBoxSize'?: Array | undefined | null;
- readonly 'bind:devicePixelContentBoxSize'?: Array | undefined | null;
readonly 'bind:focused'?: boolean | undefined | null;
- readonly 'bind:clientWidth'?: number | undefined | null;
- readonly 'bind:clientHeight'?: number | undefined | null;
readonly 'bind:offsetWidth'?: number | undefined | null;
readonly 'bind:offsetHeight'?: number | undefined | null;
diff --git a/packages/svelte/messages/compile-warnings/template.md b/packages/svelte/messages/compile-warnings/template.md
index 8af5aa2b98..3650e07b47 100644
--- a/packages/svelte/messages/compile-warnings/template.md
+++ b/packages/svelte/messages/compile-warnings/template.md
@@ -67,7 +67,7 @@ In HTML, there's [no such thing as a self-closing tag](https://jakearchibald.com
```
-Some templating languages (including Svelte) will 'fix' HTML by turning `` into ``. Others adhere to the spec. Both result in ambiguity and confusion when copy-pasting code between different contexts, and as such Svelte prompts you to resolve the ambiguity directly by having an explicit closing tag.
+Some templating languages (including Svelte) will 'fix' HTML by turning `` into ``. Others adhere to the spec. Both result in ambiguity and confusion when copy-pasting code between different contexts, so Svelte prompts you to resolve the ambiguity directly by having an explicit closing tag.
To automate this, run the dedicated migration:
diff --git a/packages/svelte/package.json b/packages/svelte/package.json
index c781eda8c8..8904c103e3 100644
--- a/packages/svelte/package.json
+++ b/packages/svelte/package.json
@@ -2,7 +2,7 @@
"name": "svelte",
"description": "Cybernetically enhanced web apps",
"license": "MIT",
- "version": "5.37.0",
+ "version": "5.38.1",
"type": "module",
"types": "./types/index.d.ts",
"engines": {
@@ -141,6 +141,7 @@
"build": "node scripts/process-messages && rollup -c && pnpm generate:types && node scripts/check-treeshakeability.js",
"dev": "node scripts/process-messages -w & rollup -cw",
"check": "tsc --project tsconfig.runtime.json && tsc && cd ./tests/types && tsc",
+ "check:tsgo": "tsgo --project tsconfig.runtime.json --skipLibCheck && tsgo --skipLibCheck",
"check:watch": "tsc --watch",
"generate:version": "node ./scripts/generate-version.js",
"generate:types": "node ./scripts/generate-types.js && tsc -p tsconfig.generated.json",
@@ -165,7 +166,7 @@
"vitest": "^2.1.9"
},
"dependencies": {
- "@ampproject/remapping": "^2.3.0",
+ "@jridgewell/remapping": "^2.3.4",
"@jridgewell/sourcemap-codec": "^1.5.0",
"@sveltejs/acorn-typescript": "^1.0.5",
"@types/estree": "^1.0.5",
diff --git a/packages/svelte/scripts/generate-types.js b/packages/svelte/scripts/generate-types.js
index c558a2bbf7..0ee6004d4a 100644
--- a/packages/svelte/scripts/generate-types.js
+++ b/packages/svelte/scripts/generate-types.js
@@ -26,9 +26,11 @@ await createBundle({
// so that types/properties with `@internal` (and its dependencies) are removed from the output
stripInternal: true,
paths: Object.fromEntries(
- Object.entries(pkg.imports).map(([key, value]) => {
- return [key, [value.types ?? value.default ?? value]];
- })
+ Object.entries(pkg.imports).map(
+ /** @param {[string,any]} import */ ([key, value]) => {
+ return [key, [value.types ?? value.default ?? value]];
+ }
+ )
)
},
modules: {
diff --git a/packages/svelte/src/ambient.d.ts b/packages/svelte/src/ambient.d.ts
index ad32eaa56f..59128e45f0 100644
--- a/packages/svelte/src/ambient.d.ts
+++ b/packages/svelte/src/ambient.d.ts
@@ -229,7 +229,7 @@ declare namespace $derived {
*
* If you return a function from the effect, it will be called right before the effect is run again, or when the component is unmounted.
*
- * Does not run during server side rendering.
+ * Does not run during server-side rendering.
*
* https://svelte.dev/docs/svelte/$effect
* @param fn The function to execute
@@ -248,7 +248,7 @@ declare namespace $effect {
*
* If you return a function from the effect, it will be called right before the effect is run again, or when the component is unmounted.
*
- * Does not run during server side rendering.
+ * Does not run during server-side rendering.
*
* https://svelte.dev/docs/svelte/$effect#$effect.pre
* @param fn The function to execute
diff --git a/packages/svelte/src/compiler/legacy.js b/packages/svelte/src/compiler/legacy.js
index 85345bca4a..b1bbcfcf74 100644
--- a/packages/svelte/src/compiler/legacy.js
+++ b/packages/svelte/src/compiler/legacy.js
@@ -55,7 +55,9 @@ export function convert(source, ast) {
// Insert svelte:options back into the root nodes
if (/** @type {any} */ (options)?.__raw__) {
- let idx = node.fragment.nodes.findIndex((node) => options.end <= node.start);
+ let idx = node.fragment.nodes.findIndex(
+ (node) => /** @type {any} */ (options).end <= node.start
+ );
if (idx === -1) {
idx = node.fragment.nodes.length;
}
diff --git a/packages/svelte/src/compiler/migrate/index.js b/packages/svelte/src/compiler/migrate/index.js
index 6b2e6cda70..eb0e4eff8c 100644
--- a/packages/svelte/src/compiler/migrate/index.js
+++ b/packages/svelte/src/compiler/migrate/index.js
@@ -1707,14 +1707,14 @@ function extract_type_and_comment(declarator, state, path) {
}
// Ensure modifiers are applied in the same order as Svelte 4
-const modifier_order = [
+const modifier_order = /** @type {const} */ ([
'preventDefault',
'stopPropagation',
'stopImmediatePropagation',
'self',
'trusted',
'once'
-];
+]);
/**
* @param {AST.RegularElement | AST.SvelteElement | AST.SvelteWindow | AST.SvelteDocument | AST.SvelteBody} element
diff --git a/packages/svelte/src/compiler/phases/1-parse/index.js b/packages/svelte/src/compiler/phases/1-parse/index.js
index f5e0693b31..8f7ef76be5 100644
--- a/packages/svelte/src/compiler/phases/1-parse/index.js
+++ b/packages/svelte/src/compiler/phases/1-parse/index.js
@@ -1,5 +1,4 @@
/** @import { AST } from '#compiler' */
-/** @import { Comment } from 'estree' */
// @ts-expect-error acorn type definitions are borked in the release we use
import { isIdentifierStart, isIdentifierChar } from 'acorn';
import fragment from './state/fragment.js';
diff --git a/packages/svelte/src/compiler/phases/1-parse/utils/create.js b/packages/svelte/src/compiler/phases/1-parse/utils/create.js
index 6030f1bd7b..2fba918f20 100644
--- a/packages/svelte/src/compiler/phases/1-parse/utils/create.js
+++ b/packages/svelte/src/compiler/phases/1-parse/utils/create.js
@@ -10,7 +10,8 @@ export function create_fragment(transparent = false) {
nodes: [],
metadata: {
transparent,
- dynamic: false
+ dynamic: false,
+ has_await: false
}
};
}
diff --git a/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js b/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js
index 79e8fbb02c..8a4d8cb350 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js
+++ b/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js
@@ -9,8 +9,8 @@ import {
import { regex_ends_with_whitespace, regex_starts_with_whitespace } from '../../patterns.js';
import { get_attribute_chunks, is_text_attribute } from '../../../utils/ast.js';
-/** @typedef {NODE_PROBABLY_EXISTS | NODE_DEFINITELY_EXISTS} NodeExistsValue */
-/** @typedef {FORWARD | BACKWARD} Direction */
+/** @typedef {typeof NODE_PROBABLY_EXISTS | typeof NODE_DEFINITELY_EXISTS} NodeExistsValue */
+/** @typedef {typeof FORWARD | typeof BACKWARD} Direction */
const NODE_PROBABLY_EXISTS = 0;
const NODE_DEFINITELY_EXISTS = 1;
diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js
index cd44fd998a..92b89c588e 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/index.js
+++ b/packages/svelte/src/compiler/phases/2-analyze/index.js
@@ -37,6 +37,7 @@ import { ExportNamedDeclaration } from './visitors/ExportNamedDeclaration.js';
import { ExportSpecifier } from './visitors/ExportSpecifier.js';
import { ExpressionStatement } from './visitors/ExpressionStatement.js';
import { ExpressionTag } from './visitors/ExpressionTag.js';
+import { Fragment } from './visitors/Fragment.js';
import { FunctionDeclaration } from './visitors/FunctionDeclaration.js';
import { FunctionExpression } from './visitors/FunctionExpression.js';
import { HtmlTag } from './visitors/HtmlTag.js';
@@ -156,6 +157,7 @@ const visitors = {
ExportSpecifier,
ExpressionStatement,
ExpressionTag,
+ Fragment,
FunctionDeclaration,
FunctionExpression,
HtmlTag,
@@ -295,11 +297,12 @@ export function analyze_module(source, options) {
// TODO the following are not needed for modules, but we have to pass them in order to avoid type error,
// and reducing the type would result in a lot of tedious type casts elsewhere - find a good solution one day
ast_type: /** @type {any} */ (null),
- component_slots: new Set(),
+ component_slots: /** @type {Set} */ (new Set()),
expression: null,
function_depth: 0,
has_props_rune: false,
options: /** @type {ValidatedCompileOptions} */ (options),
+ fragment: null,
parent_element: null,
reactive_statement: null
},
@@ -526,7 +529,6 @@ export function analyze_component(root, source, options) {
has_global: false
},
source,
- undefined_exports: new Map(),
snippet_renderers: new Map(),
snippets: new Set(),
async_deriveds: new Set()
@@ -688,6 +690,7 @@ export function analyze_component(root, source, options) {
analysis,
options,
ast_type: ast === instance.ast ? 'instance' : ast === template.ast ? 'template' : 'module',
+ fragment: ast === template.ast ? ast : null,
parent_element: null,
has_props_rune: false,
component_slots: new Set(),
@@ -753,6 +756,7 @@ export function analyze_component(root, source, options) {
scopes,
analysis,
options,
+ fragment: ast === template.ast ? ast : null,
parent_element: null,
has_props_rune: false,
ast_type: ast === instance.ast ? 'instance' : ast === template.ast ? 'template' : 'module',
@@ -787,9 +791,15 @@ export function analyze_component(root, source, options) {
if (node.type === 'ExportNamedDeclaration' && node.specifiers !== null && node.source == null) {
for (const specifier of node.specifiers) {
if (specifier.local.type !== 'Identifier') continue;
-
- const binding = analysis.module.scope.get(specifier.local.name);
- if (!binding) e.export_undefined(specifier, specifier.local.name);
+ const name = specifier.local.name;
+ const binding = analysis.module.scope.get(name);
+ if (!binding) {
+ if ([...analysis.snippets].find((snippet) => snippet.expression.name === name)) {
+ e.snippet_invalid_export(specifier);
+ } else {
+ e.export_undefined(specifier, name);
+ }
+ }
}
}
}
diff --git a/packages/svelte/src/compiler/phases/2-analyze/types.d.ts b/packages/svelte/src/compiler/phases/2-analyze/types.d.ts
index 080239bac0..2d99a2e155 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/types.d.ts
+++ b/packages/svelte/src/compiler/phases/2-analyze/types.d.ts
@@ -8,6 +8,7 @@ export interface AnalysisState {
analysis: ComponentAnalysis;
options: ValidatedCompileOptions;
ast_type: 'instance' | 'template' | 'module';
+ fragment: AST.Fragment | null;
/**
* Tag name of the parent element. `null` if the parent is `svelte:element`, `#snippet`, a component or the root.
* Parent doesn't necessarily mean direct path predecessor because there could be `#each`, `#if` etc in-between.
diff --git a/packages/svelte/src/compiler/phases/2-analyze/utils/check_graph_for_cycles.js b/packages/svelte/src/compiler/phases/2-analyze/utils/check_graph_for_cycles.js
index 67201d4825..83959248fe 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/utils/check_graph_for_cycles.js
+++ b/packages/svelte/src/compiler/phases/2-analyze/utils/check_graph_for_cycles.js
@@ -14,6 +14,7 @@ export default function check_graph_for_cycles(edges) {
}, new Map());
const visited = new Set();
+ /** @type {Set} */
const on_stack = new Set();
/** @type {Array>} */
const cycles = [];
diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js
index af7d0307e9..b2f59b849b 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js
+++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js
@@ -11,6 +11,15 @@ export function AwaitExpression(node, context) {
if (context.state.expression) {
context.state.expression.has_await = true;
+
+ if (
+ context.state.fragment &&
+ // TODO there's probably a better way to do this
+ context.path.some((node) => node.type === 'ConstTag')
+ ) {
+ context.state.fragment.metadata.has_await = true;
+ }
+
suspend = true;
}
diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/ClassBody.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/ClassBody.js
index 2bfc1dbce3..dd21637174 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/visitors/ClassBody.js
+++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/ClassBody.js
@@ -57,7 +57,7 @@ export function ClassBody(node, context) {
e.state_field_duplicate(node, name);
}
- const _key = (key.type === 'PrivateIdentifier' ? '#' : '') + name;
+ const _key = (node.type === 'AssignmentExpression' || !node.static ? '' : '@') + name;
const field = fields.get(_key);
// if there's already a method or assigned field, error
@@ -78,7 +78,7 @@ export function ClassBody(node, context) {
for (const child of node.body) {
if (child.type === 'PropertyDefinition' && !child.computed && !child.static) {
handle(child, child.key, child.value);
- const key = (child.key.type === 'PrivateIdentifier' ? '#' : '') + get_name(child.key);
+ const key = /** @type {string} */ (get_name(child.key));
const field = fields.get(key);
if (!field) {
fields.set(key, [child.value ? 'assigned_prop' : 'prop']);
@@ -91,7 +91,7 @@ export function ClassBody(node, context) {
if (child.kind === 'constructor') {
constructor = child;
} else if (!child.computed) {
- const key = (child.key.type === 'PrivateIdentifier' ? '#' : '') + get_name(child.key);
+ const key = (child.static ? '@' : '') + get_name(child.key);
const field = fields.get(key);
if (!field) {
fields.set(key, [child.kind]);
diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/Fragment.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/Fragment.js
new file mode 100644
index 0000000000..02d780dc0d
--- /dev/null
+++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/Fragment.js
@@ -0,0 +1,10 @@
+/** @import { AST } from '#compiler' */
+/** @import { Context } from '../types.js' */
+
+/**
+ * @param {AST.Fragment} node
+ * @param {Context} context
+ */
+export function Fragment(node, context) {
+ context.next({ ...context.state, fragment: node });
+}
diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/SnippetBlock.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/SnippetBlock.js
index 2f6bbd785a..7930c2b1a7 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/visitors/SnippetBlock.js
+++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/SnippetBlock.js
@@ -35,11 +35,6 @@ export function SnippetBlock(node, context) {
if (can_hoist) {
const binding = /** @type {Binding} */ (context.state.scope.get(name));
context.state.analysis.module.scope.declarations.set(name, binding);
- } else {
- const undefined_export = context.state.analysis.undefined_exports.get(name);
- if (undefined_export) {
- e.snippet_invalid_export(undefined_export);
- }
}
node.metadata.can_hoist = can_hoist;
diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/a11y/index.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/a11y/index.js
index f45a6c9a80..e2f84290e5 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/a11y/index.js
+++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/a11y/index.js
@@ -599,7 +599,7 @@ function has_disabled_attribute(attribute_map) {
/**
* @param {string} tag_name
* @param {Map} attribute_map
- * @returns {ElementInteractivity[keyof ElementInteractivity]}
+ * @returns {typeof ElementInteractivity[keyof typeof ElementInteractivity]}
*/
function element_interactivity(tag_name, attribute_map) {
if (
diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/component.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/component.js
index aca87fab81..6d09398fb7 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/component.js
+++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/component.js
@@ -145,6 +145,7 @@ export function visit_component(node, context) {
if (slot_name !== 'default') comments = [];
}
+ /** @type {Set} */
const component_slots = new Set();
for (const slot_name in nodes) {
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js
index a56aca9c5f..166207f66a 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js
@@ -26,6 +26,7 @@ import { DebugTag } from './visitors/DebugTag.js';
import { EachBlock } from './visitors/EachBlock.js';
import { ExportNamedDeclaration } from './visitors/ExportNamedDeclaration.js';
import { ExpressionStatement } from './visitors/ExpressionStatement.js';
+import { ForOfStatement } from './visitors/ForOfStatement.js';
import { Fragment } from './visitors/Fragment.js';
import { FunctionDeclaration } from './visitors/FunctionDeclaration.js';
import { FunctionExpression } from './visitors/FunctionExpression.js';
@@ -103,6 +104,7 @@ const visitors = {
EachBlock,
ExportNamedDeclaration,
ExpressionStatement,
+ ForOfStatement,
Fragment,
FunctionDeclaration,
FunctionExpression,
@@ -170,6 +172,7 @@ export function client_component(analysis, options) {
// these are set inside the `Fragment` visitor, and cannot be used until then
init: /** @type {any} */ (null),
+ consts: /** @type {any} */ (null),
update: /** @type {any} */ (null),
after_update: /** @type {any} */ (null),
template: /** @type {any} */ (null),
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts b/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts
index e691be169b..59c024dfb7 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts
+++ b/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts
@@ -6,7 +6,8 @@ import type {
Expression,
AssignmentExpression,
UpdateExpression,
- VariableDeclaration
+ VariableDeclaration,
+ Declaration
} from 'estree';
import type { AST, Namespace, ValidatedCompileOptions } from '#compiler';
import type { TransformState } from '../types.js';
@@ -57,6 +58,8 @@ export interface ComponentClientTransformState extends ClientTransformState {
readonly update: Statement[];
/** Stuff that happens after the render effect (control blocks, dynamic elements, bindings, actions, etc) */
readonly after_update: Statement[];
+ /** Transformed `{@const }` declarations */
+ readonly consts: Statement[];
/** Memoized expressions */
readonly memoizer: Memoizer;
/** The HTML template string */
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/utils.js b/packages/svelte/src/compiler/phases/3-transform/client/utils.js
index 6d9dac8a33..19a4342b5e 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/utils.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/utils.js
@@ -1,4 +1,4 @@
-/** @import { ArrowFunctionExpression, AssignmentExpression, Expression, FunctionDeclaration, FunctionExpression, Identifier, Node, Pattern, UpdateExpression } from 'estree' */
+/** @import { ArrowFunctionExpression, AssignmentExpression, BlockStatement, Expression, FunctionDeclaration, FunctionExpression, Identifier, Node, Pattern, UpdateExpression } from 'estree' */
/** @import { Binding } from '#compiler' */
/** @import { ClientTransformState, ComponentClientTransformState, ComponentContext } from './types.js' */
/** @import { Analysis } from '../../types.js' */
@@ -289,8 +289,15 @@ export function should_proxy(node, scope) {
/**
* Svelte legacy mode should use safe equals in most places, runes mode shouldn't
* @param {ComponentClientTransformState} state
- * @param {Expression} arg
+ * @param {Expression | BlockStatement} expression
+ * @param {boolean} [async]
*/
-export function create_derived(state, arg) {
- return b.call(state.analysis.runes ? '$.derived' : '$.derived_safe_equal', arg);
+export function create_derived(state, expression, async = false) {
+ const thunk = b.thunk(expression, async);
+
+ if (async) {
+ return b.call(b.await(b.call('$.save', b.call('$.async_derived', thunk))));
+ } else {
+ return b.call(state.analysis.runes ? '$.derived' : '$.derived_safe_equal', thunk);
+ }
}
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitBlock.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitBlock.js
index c550c8e17b..e2e8e93f76 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitBlock.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitBlock.js
@@ -1,7 +1,7 @@
/** @import { BlockStatement, Pattern, Statement } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentClientTransformState, ComponentContext } from '../types' */
-import { extract_identifiers } from '../../../../utils/ast.js';
+import { extract_identifiers, is_expression_async } from '../../../../utils/ast.js';
import * as b from '#compiler/builders';
import { create_derived } from '../utils.js';
import { get_value } from './shared/declarations.js';
@@ -15,7 +15,10 @@ export function AwaitBlock(node, context) {
context.state.template.push_comment();
// Visit {#await } first to ensure that scopes are in the correct order
- const expression = b.thunk(build_expression(context, node.expression, node.metadata.expression));
+ const expression = b.thunk(
+ build_expression(context, node.expression, node.metadata.expression),
+ node.metadata.expression.has_await
+ );
let then_block;
let catch_block;
@@ -93,13 +96,13 @@ function create_derived_block_argument(node, context) {
b.return(b.object(identifiers.map((identifier) => b.prop('init', identifier, identifier))))
]);
- const declarations = [b.var(value, create_derived(context.state, b.thunk(block)))];
+ const declarations = [b.var(value, create_derived(context.state, block))];
for (const id of identifiers) {
context.state.transform[id.name] = { read: get_value };
declarations.push(
- b.var(id, create_derived(context.state, b.thunk(b.member(b.call('$.get', value), id))))
+ b.var(id, create_derived(context.state, b.member(b.call('$.get', value), id)))
);
}
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/CallExpression.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/CallExpression.js
index 3e2f1414e6..c126742d3c 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/CallExpression.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/CallExpression.js
@@ -82,7 +82,9 @@ export function CallExpression(node, context) {
['debug', 'dir', 'error', 'group', 'groupCollapsed', 'info', 'log', 'trace', 'warn'].includes(
node.callee.property.name
) &&
- node.arguments.some((arg) => arg.type !== 'Literal') // TODO more cases?
+ node.arguments.some(
+ (arg) => arg.type === 'SpreadElement' || context.state.scope.evaluate(arg).has_unknown
+ )
) {
return b.call(
node.callee,
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ConstTag.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ConstTag.js
index 34acdd6bb9..b550dae890 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ConstTag.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ConstTag.js
@@ -16,21 +16,26 @@ export function ConstTag(node, context) {
const declaration = node.declaration.declarations[0];
// TODO we can almost certainly share some code with $derived(...)
if (declaration.id.type === 'Identifier') {
- const init = build_expression(context, declaration.init, node.metadata.expression);
- let expression = create_derived(context.state, b.thunk(init));
+ const init = build_expression(
+ { ...context, state: { ...context.state, in_derived: true } },
+ declaration.init,
+ node.metadata.expression
+ );
+
+ let expression = create_derived(context.state, init, node.metadata.expression.has_await);
if (dev) {
expression = b.call('$.tag', expression, b.literal(declaration.id.name));
}
- context.state.init.push(b.const(declaration.id, expression));
+ context.state.consts.push(b.const(declaration.id, expression));
context.state.transform[declaration.id.name] = { read: get_value };
// we need to eagerly evaluate the expression in order to hit any
// 'Cannot access x before initialization' errors
if (dev) {
- context.state.init.push(b.stmt(b.call('$.get', declaration.id)));
+ context.state.consts.push(b.stmt(b.call('$.get', declaration.id)));
}
} else {
const identifiers = extract_identifiers(declaration.id);
@@ -44,7 +49,11 @@ export function ConstTag(node, context) {
delete transform[node.name];
}
- const child_state = { ...context.state, transform };
+ const child_state = /** @type {ComponentContext['state']} */ ({
+ ...context.state,
+ transform,
+ in_derived: true
+ });
// TODO optimise the simple `{ x } = y` case — we can just return `y`
// instead of destructuring it only to return a new object
@@ -53,26 +62,24 @@ export function ConstTag(node, context) {
declaration.init,
node.metadata.expression
);
- const fn = b.arrow(
- [],
- b.block([
- b.const(/** @type {Pattern} */ (context.visit(declaration.id, child_state)), init),
- b.return(b.object(identifiers.map((node) => b.prop('init', node, node))))
- ])
- );
- let expression = create_derived(context.state, fn);
+ const block = b.block([
+ b.const(/** @type {Pattern} */ (context.visit(declaration.id, child_state)), init),
+ b.return(b.object(identifiers.map((node) => b.prop('init', node, node))))
+ ]);
+
+ let expression = create_derived(context.state, block, node.metadata.expression.has_await);
if (dev) {
expression = b.call('$.tag', expression, b.literal('[@const]'));
}
- context.state.init.push(b.const(tmp, expression));
+ context.state.consts.push(b.const(tmp, expression));
// we need to eagerly evaluate the expression in order to hit any
// 'Cannot access x before initialization' errors
if (dev) {
- context.state.init.push(b.stmt(b.call('$.get', tmp)));
+ context.state.consts.push(b.stmt(b.call('$.get', tmp)));
}
for (const node of identifiers) {
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ForOfStatement.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ForOfStatement.js
new file mode 100644
index 0000000000..a5d2751812
--- /dev/null
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ForOfStatement.js
@@ -0,0 +1,20 @@
+/** @import { Expression, ForOfStatement, Pattern, Statement, VariableDeclaration } from 'estree' */
+/** @import { ComponentContext } from '../types' */
+import * as b from '#compiler/builders';
+import { dev, is_ignored } from '../../../../state.js';
+
+/**
+ * @param {ForOfStatement} node
+ * @param {ComponentContext} context
+ */
+export function ForOfStatement(node, context) {
+ if (node.await && dev && !is_ignored(node, 'await_reactivity_loss')) {
+ const left = /** @type {VariableDeclaration | Pattern} */ (context.visit(node.left));
+ const argument = /** @type {Expression} */ (context.visit(node.right));
+ const body = /** @type {Statement} */ (context.visit(node.body));
+ const right = b.call('$.for_await_track_reactivity_loss', argument);
+ return b.for_of(left, right, body, true);
+ }
+
+ context.next();
+}
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Fragment.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Fragment.js
index 0b10c02ffb..c7c576101e 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Fragment.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Fragment.js
@@ -48,8 +48,10 @@ export function Fragment(node, context) {
const is_single_child_not_needing_template =
trimmed.length === 1 &&
(trimmed[0].type === 'SvelteFragment' || trimmed[0].type === 'TitleElement');
+ const has_await = context.state.init !== null && (node.metadata.has_await || false);
const template_name = context.state.scope.root.unique('root'); // TODO infer name from parent
+ const unsuspend = b.id('$$unsuspend');
/** @type {Statement[]} */
const body = [];
@@ -61,6 +63,7 @@ export function Fragment(node, context) {
const state = {
...context.state,
init: [],
+ consts: [],
update: [],
after_update: [],
memoizer: new Memoizer(),
@@ -76,11 +79,6 @@ export function Fragment(node, context) {
context.visit(node, state);
}
- if (is_text_first) {
- // skip over inserted comment
- body.push(b.stmt(b.call('$.next')));
- }
-
if (is_single_element) {
const element = /** @type {AST.RegularElement} */ (trimmed[0]);
@@ -96,13 +94,13 @@ export function Fragment(node, context) {
const template = transform_template(state, namespace, flags);
state.hoisted.push(b.var(template_name, template));
- body.push(b.var(id, b.call(template_name)));
+ state.init.unshift(b.var(id, b.call(template_name)));
close = b.stmt(b.call('$.append', b.id('$$anchor'), id));
} else if (is_single_child_not_needing_template) {
context.visit(trimmed[0], state);
} else if (trimmed.length === 1 && trimmed[0].type === 'Text') {
const id = b.id(context.state.scope.generate('text'));
- body.push(b.var(id, b.call('$.text', b.literal(trimmed[0].data))));
+ state.init.unshift(b.var(id, b.call('$.text', b.literal(trimmed[0].data))));
close = b.stmt(b.call('$.append', b.id('$$anchor'), id));
} else if (trimmed.length > 0) {
const id = b.id(context.state.scope.generate('fragment'));
@@ -120,7 +118,7 @@ export function Fragment(node, context) {
state
});
- body.push(b.var(id, b.call('$.text')));
+ state.init.unshift(b.var(id, b.call('$.text')));
close = b.stmt(b.call('$.append', b.id('$$anchor'), id));
} else {
if (is_standalone) {
@@ -140,12 +138,12 @@ export function Fragment(node, context) {
if (state.template.nodes.length === 1 && state.template.nodes[0].type === 'comment') {
// special case — we can use `$.comment` instead of creating a unique template
- body.push(b.var(id, b.call('$.comment')));
+ state.init.unshift(b.var(id, b.call('$.comment')));
} else {
const template = transform_template(state, namespace, flags);
state.hoisted.push(b.var(template_name, template));
- body.push(b.var(id, b.call(template_name)));
+ state.init.unshift(b.var(id, b.call(template_name)));
}
close = b.stmt(b.call('$.append', b.id('$$anchor'), id));
@@ -153,6 +151,21 @@ export function Fragment(node, context) {
}
}
+ if (has_await) {
+ body.push(b.var(unsuspend, b.call('$.suspend')));
+ }
+
+ body.push(...state.consts);
+
+ if (has_await) {
+ body.push(b.if(b.call('$.aborted'), b.return()));
+ }
+
+ if (is_text_first) {
+ // skip over inserted comment
+ body.push(b.stmt(b.call('$.next')));
+ }
+
body.push(...state.init);
if (state.update.length > 0) {
@@ -168,5 +181,9 @@ export function Fragment(node, context) {
body.push(close);
}
+ if (has_await) {
+ body.push(b.stmt(b.call(unsuspend)));
+ }
+
return b.block(body);
}
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/LetDirective.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/LetDirective.js
index abdbc381d9..f33febeeb2 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/LetDirective.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/LetDirective.js
@@ -46,9 +46,6 @@ export function LetDirective(node, context) {
read: (node) => b.call('$.get', node)
};
- return b.const(
- name,
- create_derived(context.state, b.thunk(b.member(b.id('$$slotProps'), node.name)))
- );
+ return b.const(name, create_derived(context.state, b.member(b.id('$$slotProps'), node.name)));
}
}
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/OnDirective.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/OnDirective.js
index 7a66a8ecbb..0ee3b0fb10 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/OnDirective.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/OnDirective.js
@@ -3,14 +3,14 @@
import * as b from '#compiler/builders';
import { build_event, build_event_handler } from './shared/events.js';
-const modifiers = [
+const modifiers = /** @type {const} */ ([
'stopPropagation',
'stopImmediatePropagation',
'preventDefault',
'self',
'trusted',
'once'
-];
+]);
/**
* @param {AST.OnDirective} node
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SnippetBlock.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SnippetBlock.js
index 203cf62b37..895522d47a 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SnippetBlock.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SnippetBlock.js
@@ -14,6 +14,7 @@ export function SnippetBlock(node, context) {
// TODO hoist where possible
/** @type {(Identifier | AssignmentPattern)[]} */
const args = [b.id('$$anchor')];
+ const has_await = node.body.metadata.has_await || false;
/** @type {BlockStatement} */
let body;
@@ -21,10 +22,6 @@ export function SnippetBlock(node, context) {
/** @type {Statement[]} */
const declarations = [];
- if (dev) {
- declarations.push(b.stmt(b.call('$.validate_snippet_args', b.spread(b.id('arguments')))));
- }
-
const transform = { ...context.state.transform };
const child_state = { ...context.state, transform };
@@ -72,16 +69,21 @@ export function SnippetBlock(node, context) {
}
}
}
-
+ const block = /** @type {BlockStatement} */ (context.visit(node.body, child_state)).body;
body = b.block([
+ dev ? b.stmt(b.call('$.validate_snippet_args', b.spread(b.id('arguments')))) : b.empty,
...declarations,
- .../** @type {BlockStatement} */ (context.visit(node.body, child_state)).body
+ ...block
]);
// in dev we use a FunctionExpression (not arrow function) so we can use `arguments`
let snippet = dev
- ? b.call('$.wrap_snippet', b.id(context.state.analysis.name), b.function(null, args, body))
- : b.arrow(args, body);
+ ? b.call(
+ '$.wrap_snippet',
+ b.id(context.state.analysis.name),
+ b.function(null, args, body, has_await)
+ )
+ : b.arrow(args, body, has_await);
const declaration = b.const(node.expression, snippet);
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteBoundary.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteBoundary.js
index d37b990440..70df022355 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteBoundary.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteBoundary.js
@@ -43,7 +43,7 @@ export function SvelteBoundary(node, context) {
// to resolve this we cheat: we duplicate const tags inside snippets
for (const child of node.fragment.nodes) {
if (child.type === 'ConstTag') {
- context.visit(child, { ...context.state, init: const_tags });
+ context.visit(child, { ...context.state, consts: const_tags });
}
}
diff --git a/packages/svelte/src/compiler/phases/3-transform/shared/assignments.js b/packages/svelte/src/compiler/phases/3-transform/shared/assignments.js
index 175b44f4fe..0853757775 100644
--- a/packages/svelte/src/compiler/phases/3-transform/shared/assignments.js
+++ b/packages/svelte/src/compiler/phases/3-transform/shared/assignments.js
@@ -65,13 +65,14 @@ export function visit_assignment_expression(node, context, build_assignment) {
statements.push(b.return(rhs));
}
- const iife = b.arrow([rhs], b.block(statements));
-
- const iife_is_async =
+ const async =
is_expression_async(value) ||
assignments.some((assignment) => is_expression_async(assignment));
- return iife_is_async ? b.await(b.call(b.async(iife), value)) : b.call(iife, value);
+ const iife = b.arrow([rhs], b.block(statements), async);
+ const call = b.call(iife, value);
+
+ return async ? b.await(call) : call;
}
const sequence = b.sequence(assignments);
diff --git a/packages/svelte/src/compiler/phases/scope.js b/packages/svelte/src/compiler/phases/scope.js
index 700e098e45..f88f5ef8b1 100644
--- a/packages/svelte/src/compiler/phases/scope.js
+++ b/packages/svelte/src/compiler/phases/scope.js
@@ -22,7 +22,7 @@ const NUMBER = Symbol('number');
const STRING = Symbol('string');
const FUNCTION = Symbol('string');
-/** @type {Record} */
+/** @type {Record} */
const globals = {
BigInt: [NUMBER],
'Math.min': [NUMBER, Math.min],
@@ -180,6 +180,13 @@ class Evaluation {
*/
is_known = true;
+ /**
+ * True if the possible values contains `UNKNOWN`
+ * @readonly
+ * @type {boolean}
+ */
+ has_unknown = false;
+
/**
* True if the value is known to not be null/undefined
* @readonly
@@ -540,6 +547,10 @@ class Evaluation {
if (value == null || value === UNKNOWN) {
this.is_defined = false;
}
+
+ if (value === UNKNOWN) {
+ this.has_unknown = true;
+ }
}
if (this.values.size > 1 || typeof this.value === 'symbol') {
diff --git a/packages/svelte/src/compiler/phases/types.d.ts b/packages/svelte/src/compiler/phases/types.d.ts
index 4cbbc362c7..4da78f1482 100644
--- a/packages/svelte/src/compiler/phases/types.d.ts
+++ b/packages/svelte/src/compiler/phases/types.d.ts
@@ -95,7 +95,6 @@ export interface ComponentAnalysis extends Analysis {
};
/** @deprecated use `source` from `state.js` instead */
source: string;
- undefined_exports: Map;
/**
* Every render tag/component, and whether it could be definitively resolved or not
*/
diff --git a/packages/svelte/src/compiler/preprocess/index.js b/packages/svelte/src/compiler/preprocess/index.js
index afef898471..429c2dcff1 100644
--- a/packages/svelte/src/compiler/preprocess/index.js
+++ b/packages/svelte/src/compiler/preprocess/index.js
@@ -1,6 +1,6 @@
/** @import { Processed, Preprocessor, MarkupPreprocessor, PreprocessorGroup } from './public.js' */
/** @import { SourceUpdate, Source } from './private.js' */
-/** @import { DecodedSourceMap, RawSourceMap } from '@ampproject/remapping' */
+/** @import { DecodedSourceMap, RawSourceMap } from '@jridgewell/remapping' */
import { getLocator } from 'locate-character';
import {
MappedCode,
@@ -25,7 +25,7 @@ class PreprocessResult {
// sourcemap_list is sorted in reverse order from last map (index 0) to first map (index -1)
// so we use sourcemap_list.unshift() to add new maps
- // https://github.com/ampproject/remapping#multiple-transformations-of-a-file
+ // https://github.com/jridgewell/sourcemaps/tree/main/packages/remapping#multiple-transformations-of-a-file
/**
* @default []
diff --git a/packages/svelte/src/compiler/preprocess/private.d.ts b/packages/svelte/src/compiler/preprocess/private.d.ts
index 5d8c5ed44e..2c331cee12 100644
--- a/packages/svelte/src/compiler/preprocess/private.d.ts
+++ b/packages/svelte/src/compiler/preprocess/private.d.ts
@@ -1,4 +1,4 @@
-import { DecodedSourceMap } from '@ampproject/remapping';
+import { DecodedSourceMap } from '@jridgewell/remapping';
import { Location } from 'locate-character';
import { MappedCode } from '../utils/mapped_code.js';
diff --git a/packages/svelte/src/compiler/state.js b/packages/svelte/src/compiler/state.js
index 725d03b802..6d9873eb30 100644
--- a/packages/svelte/src/compiler/state.js
+++ b/packages/svelte/src/compiler/state.js
@@ -87,7 +87,7 @@ export function pop_ignore() {
/**
* @param {AST.SvelteNode | NodeLike} node
- * @param {import('../constants.js').IGNORABLE_RUNTIME_WARNINGS[number]} code
+ * @param {typeof import('../constants.js').IGNORABLE_RUNTIME_WARNINGS[number]} code
* @returns
*/
export function is_ignored(node, code) {
diff --git a/packages/svelte/src/compiler/types/template.d.ts b/packages/svelte/src/compiler/types/template.d.ts
index de06a41469..060df2dcb2 100644
--- a/packages/svelte/src/compiler/types/template.d.ts
+++ b/packages/svelte/src/compiler/types/template.d.ts
@@ -56,6 +56,7 @@ export namespace AST {
* Whether or not we need to traverse into the fragment during mount/hydrate
*/
dynamic: boolean;
+ has_await: boolean;
};
}
@@ -247,7 +248,17 @@ export namespace AST {
name: string;
/** The 'y' in `on:x={y}` */
expression: null | Expression;
- modifiers: string[]; // TODO specify
+ modifiers: Array<
+ | 'capture'
+ | 'nonpassive'
+ | 'once'
+ | 'passive'
+ | 'preventDefault'
+ | 'self'
+ | 'stopImmediatePropagation'
+ | 'stopPropagation'
+ | 'trusted'
+ >;
/** @internal */
metadata: {
expression: ExpressionMetadata;
diff --git a/packages/svelte/src/compiler/utils/builders.js b/packages/svelte/src/compiler/utils/builders.js
index c241cdb445..56a5f31ffe 100644
--- a/packages/svelte/src/compiler/utils/builders.js
+++ b/packages/svelte/src/compiler/utils/builders.js
@@ -56,15 +56,6 @@ export function assignment(operator, left, right) {
return { type: 'AssignmentExpression', operator, left, right };
}
-/**
- * @template T
- * @param {T & ESTree.BaseFunction} func
- * @returns {T & ESTree.BaseFunction}
- */
-export function async(func) {
- return { ...func, async: true };
-}
-
/**
* @param {ESTree.Expression} argument
* @returns {ESTree.AwaitExpression}
@@ -214,6 +205,23 @@ export function export_default(declaration) {
return { type: 'ExportDefaultDeclaration', declaration };
}
+/**
+ * @param {ESTree.VariableDeclaration | ESTree.Pattern} left
+ * @param {ESTree.Expression} right
+ * @param {ESTree.Statement} body
+ * @param {boolean} [_await]
+ * @returns {ESTree.ForOfStatement}
+ */
+export function for_of(left, right, body, _await = false) {
+ return {
+ type: 'ForOfStatement',
+ left,
+ right,
+ body,
+ await: _await
+ };
+}
+
/**
* @param {ESTree.Identifier} id
* @param {ESTree.Pattern[]} params
@@ -580,14 +588,14 @@ export function method(kind, key, params, body, computed = false, is_static = fa
* @param {ESTree.BlockStatement} body
* @returns {ESTree.FunctionExpression}
*/
-function function_builder(id, params, body) {
+function function_builder(id, params, body, async = false) {
return {
type: 'FunctionExpression',
id,
params,
body,
generator: false,
- async: false,
+ async,
metadata: /** @type {any} */ (null) // should not be used by codegen
};
}
diff --git a/packages/svelte/src/compiler/utils/mapped_code.js b/packages/svelte/src/compiler/utils/mapped_code.js
index f23e3be245..7686ba59c6 100644
--- a/packages/svelte/src/compiler/utils/mapped_code.js
+++ b/packages/svelte/src/compiler/utils/mapped_code.js
@@ -2,8 +2,8 @@
/** @import { Processed } from '../preprocess/public.js' */
/** @import { SourceMap } from 'magic-string' */
/** @import { Source } from '../preprocess/private.js' */
-/** @import { DecodedSourceMap, SourceMapSegment, RawSourceMap } from '@ampproject/remapping' */
-import remapping from '@ampproject/remapping';
+/** @import { DecodedSourceMap, SourceMapSegment, RawSourceMap } from '@jridgewell/remapping' */
+import remapping from '@jridgewell/remapping';
import { push_array } from './push_array.js';
/**
diff --git a/packages/svelte/src/index-client.js b/packages/svelte/src/index-client.js
index 397a81c319..85eeab7de9 100644
--- a/packages/svelte/src/index-client.js
+++ b/packages/svelte/src/index-client.js
@@ -160,10 +160,14 @@ export function createEventDispatcher() {
e.lifecycle_outside_component('createEventDispatcher');
}
+ /**
+ * @param [detail]
+ * @param [options]
+ */
return (type, detail, options) => {
const events = /** @type {Record} */ (
active_component_context.s.$$events
- )?.[/** @type {any} */ (type)];
+ )?.[/** @type {string} */ (type)];
if (events) {
const callbacks = is_array(events) ? events.slice() : [events];
diff --git a/packages/svelte/src/internal/client/dev/hmr.js b/packages/svelte/src/internal/client/dev/hmr.js
index 27e2643d16..709a1b2722 100644
--- a/packages/svelte/src/internal/client/dev/hmr.js
+++ b/packages/svelte/src/internal/client/dev/hmr.js
@@ -64,7 +64,7 @@ export function hmr(original, get_source) {
// @ts-expect-error
wrapper[FILENAME] = original[FILENAME];
- // @ts-expect-error
+ // @ts-ignore
wrapper[HMR] = {
// When we accept an update, we set the original source to the new component
original,
diff --git a/packages/svelte/src/internal/client/dom/blocks/await.js b/packages/svelte/src/internal/client/dom/blocks/await.js
index 4f68db57b1..42df41041e 100644
--- a/packages/svelte/src/internal/client/dom/blocks/await.js
+++ b/packages/svelte/src/internal/client/dom/blocks/await.js
@@ -28,6 +28,8 @@ const PENDING = 0;
const THEN = 1;
const CATCH = 2;
+/** @typedef {typeof PENDING | typeof THEN | typeof CATCH} AwaitState */
+
/**
* @template V
* @param {TemplateNode} node
@@ -67,9 +69,8 @@ export function await_block(node, get_input, pending_fn, then_fn, catch_fn) {
: mutable_source(/** @type {V} */ (undefined), false, false);
var error_source = runes ? source(undefined) : mutable_source(undefined, false, false);
var resolved = false;
-
/**
- * @param {PENDING | THEN | CATCH} state
+ * @param {AwaitState} state
* @param {boolean} restore
*/
function update(state, restore) {
diff --git a/packages/svelte/src/internal/client/dom/blocks/each.js b/packages/svelte/src/internal/client/dom/blocks/each.js
index 43c75e2a37..006bf09257 100644
--- a/packages/svelte/src/internal/client/dom/blocks/each.js
+++ b/packages/svelte/src/internal/client/dom/blocks/each.js
@@ -191,7 +191,7 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f
// store a reference to the effect so that we can update the start/end nodes in reconciliation
each_effect ??= /** @type {Effect} */ (active_effect);
- array = get(each_array);
+ array = /** @type {V[]} */ (get(each_array));
var length = array.length;
if (was_empty && length === 0) {
diff --git a/packages/svelte/src/internal/client/dom/blocks/if.js b/packages/svelte/src/internal/client/dom/blocks/if.js
index 6ba9ad4936..f418d46538 100644
--- a/packages/svelte/src/internal/client/dom/blocks/if.js
+++ b/packages/svelte/src/internal/client/dom/blocks/if.js
@@ -36,7 +36,7 @@ export function if_block(node, fn, elseif = false) {
/** @type {Effect | null} */
var alternate_effect = null;
- /** @type {UNINITIALIZED | boolean | null} */
+ /** @type {typeof UNINITIALIZED | boolean | null} */
var condition = UNINITIALIZED;
var flags = elseif ? EFFECT_TRANSPARENT : 0;
diff --git a/packages/svelte/src/internal/client/dom/blocks/svelte-component.js b/packages/svelte/src/internal/client/dom/blocks/svelte-component.js
index f16da9c427..2697722b39 100644
--- a/packages/svelte/src/internal/client/dom/blocks/svelte-component.js
+++ b/packages/svelte/src/internal/client/dom/blocks/svelte-component.js
@@ -62,8 +62,10 @@ export function component(node, get_component, render_fn) {
if (defer) {
offscreen_fragment = document.createDocumentFragment();
offscreen_fragment.append((target = create_text()));
+ if (effect) {
+ /** @type {Batch} */ (current_batch).skipped_effects.add(effect);
+ }
}
-
pending_effect = branch(() => render_fn(target, component));
}
diff --git a/packages/svelte/src/internal/client/dom/elements/attributes.js b/packages/svelte/src/internal/client/dom/elements/attributes.js
index 22e532f5e4..2fa5d4541c 100644
--- a/packages/svelte/src/internal/client/dom/elements/attributes.js
+++ b/packages/svelte/src/internal/client/dom/elements/attributes.js
@@ -19,7 +19,7 @@ import { attach } from './attachments.js';
import { clsx } from '../../../shared/attributes.js';
import { set_class } from './class.js';
import { set_style } from './style.js';
-import { ATTACHMENT_KEY, NAMESPACE_HTML } from '../../../../constants.js';
+import { ATTACHMENT_KEY, NAMESPACE_HTML, UNINITIALIZED } from '../../../../constants.js';
import { block, branch, destroy_effect, effect } from '../../reactivity/effects.js';
import { init_select, select_option } from './bindings/select.js';
import { flatten } from '../../reactivity/async.js';
@@ -446,6 +446,8 @@ export function set_attributes(element, prev, next, css_hash, skip_warning = fal
) {
// @ts-ignore
element[name] = value;
+ // remove it from attributes's cache
+ if (name in attributes) attributes[name] = UNINITIALIZED;
} else if (typeof value !== 'function') {
set_attribute(element, name, value, skip_warning);
}
diff --git a/packages/svelte/src/internal/client/dom/elements/bindings/input.js b/packages/svelte/src/internal/client/dom/elements/bindings/input.js
index 7c73280dd6..67e6ff1dd2 100644
--- a/packages/svelte/src/internal/client/dom/elements/bindings/input.js
+++ b/packages/svelte/src/internal/client/dom/elements/bindings/input.js
@@ -245,6 +245,7 @@ export function bind_checked(input, get, set = get) {
* @returns {V[]}
*/
function get_binding_group_value(group, __value, checked) {
+ /** @type {Set} */
var value = new Set();
for (var i = 0; i < group.length; i += 1) {
diff --git a/packages/svelte/src/internal/client/dom/elements/events.js b/packages/svelte/src/internal/client/dom/elements/events.js
index fa3bf0b021..15544d7426 100644
--- a/packages/svelte/src/internal/client/dom/elements/events.js
+++ b/packages/svelte/src/internal/client/dom/elements/events.js
@@ -141,6 +141,13 @@ export function delegate(events) {
}
}
+// used to store the reference to the currently propagated event
+// to prevent garbage collection between microtasks in Firefox
+// If the event object is GCed too early, the expando __root property
+// set on the event object is lost, causing the event delegation
+// to process the event twice
+let last_propagated_event = null;
+
/**
* @this {EventTarget}
* @param {Event} event
@@ -153,14 +160,19 @@ export function handle_event_propagation(event) {
var path = event.composedPath?.() || [];
var current_target = /** @type {null | Element} */ (path[0] || event.target);
+ last_propagated_event = event;
+
// composedPath contains list of nodes the event has propagated through.
// We check __root to skip all nodes below it in case this is a
// parent of the __root node, which indicates that there's nested
// mounted apps. In this case we don't want to trigger events multiple times.
var path_idx = 0;
+ // the `last_propagated_event === event` check is redundant, but
+ // without it the variable will be DCE'd and things will
+ // fail mysteriously in Firefox
// @ts-expect-error is added below
- var handled_at = event.__root;
+ var handled_at = last_propagated_event === event && event.__root;
if (handled_at) {
var at_idx = path.indexOf(handled_at);
diff --git a/packages/svelte/src/internal/client/dom/template.js b/packages/svelte/src/internal/client/dom/template.js
index ebbf0039b2..265a52262f 100644
--- a/packages/svelte/src/internal/client/dom/template.js
+++ b/packages/svelte/src/internal/client/dom/template.js
@@ -156,7 +156,7 @@ export function from_mathml(content, flags) {
/**
* @param {TemplateStructure[]} structure
- * @param {NAMESPACE_SVG | NAMESPACE_MATHML | undefined} [ns]
+ * @param {typeof NAMESPACE_SVG | typeof NAMESPACE_MATHML | undefined} [ns]
*/
function fragment_from_tree(structure, ns) {
var fragment = create_fragment();
diff --git a/packages/svelte/src/internal/client/index.js b/packages/svelte/src/internal/client/index.js
index 90f0f9baac..c094c9e044 100644
--- a/packages/svelte/src/internal/client/index.js
+++ b/packages/svelte/src/internal/client/index.js
@@ -98,7 +98,11 @@ export {
props_id,
with_script
} from './dom/template.js';
-export { save, track_reactivity_loss } from './reactivity/async.js';
+export {
+ for_await_track_reactivity_loss,
+ save,
+ track_reactivity_loss
+} from './reactivity/async.js';
export { flushSync as flush, suspend } from './reactivity/batch.js';
export {
async_derived,
diff --git a/packages/svelte/src/internal/client/reactivity/async.js b/packages/svelte/src/internal/client/reactivity/async.js
index c200f10dba..2b133e5f44 100644
--- a/packages/svelte/src/internal/client/reactivity/async.js
+++ b/packages/svelte/src/internal/client/reactivity/async.js
@@ -119,6 +119,51 @@ export async function track_reactivity_loss(promise) {
};
}
+/**
+ * Used in `for await` loops in DEV, so
+ * that we can emit `await_reactivity_loss` warnings
+ * after each `async_iterator` result resolves and
+ * after the `async_iterator` return resolves (if it runs)
+ * @template T
+ * @template TReturn
+ * @param {Iterable | AsyncIterable} iterable
+ * @returns {AsyncGenerator}
+ */
+export async function* for_await_track_reactivity_loss(iterable) {
+ // This is based on the algorithms described in ECMA-262:
+ // ForIn/OfBodyEvaluation
+ // https://tc39.es/ecma262/multipage/ecmascript-language-statements-and-declarations.html#sec-runtime-semantics-forin-div-ofbodyevaluation-lhs-stmt-iterator-lhskind-labelset
+ // AsyncIteratorClose
+ // https://tc39.es/ecma262/multipage/abstract-operations.html#sec-asynciteratorclose
+
+ /** @type {AsyncIterator} */
+ // @ts-ignore
+ const iterator = iterable[Symbol.asyncIterator]?.() ?? iterable[Symbol.iterator]?.();
+
+ if (iterator === undefined) {
+ throw new TypeError('value is not async iterable');
+ }
+
+ /** Whether the completion of the iterator was "normal", meaning it wasn't ended via `break` or a similar method */
+ let normal_completion = false;
+ try {
+ while (true) {
+ const { done, value } = (await track_reactivity_loss(iterator.next()))();
+ if (done) {
+ normal_completion = true;
+ break;
+ }
+ yield value;
+ }
+ } finally {
+ // If the iterator had a normal completion and `return` is defined on the iterator, call it and return the value
+ if (normal_completion && iterator.return !== undefined) {
+ // eslint-disable-next-line no-unsafe-finally
+ return /** @type {TReturn} */ ((await track_reactivity_loss(iterator.return()))().value);
+ }
+ }
+}
+
export function unset_context() {
set_active_effect(null);
set_active_reaction(null);
diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js
index f44efa32f1..68a1555032 100644
--- a/packages/svelte/src/internal/client/reactivity/effects.js
+++ b/packages/svelte/src/internal/client/reactivity/effects.js
@@ -42,6 +42,7 @@ import { get_next_sibling } from '../dom/operations.js';
import { component_context, dev_current_component_function, dev_stack } from '../context.js';
import { Batch, schedule_effect } from './batch.js';
import { flatten } from './async.js';
+import { without_reactive_context } from '../dom/elements/bindings/shared.js';
/**
* @param {'$effect' | '$effect.pre' | '$inspect'} rune
@@ -406,7 +407,13 @@ export function destroy_effect_children(signal, remove_dom = false) {
signal.first = signal.last = null;
while (effect !== null) {
- effect.ac?.abort(STALE_REACTION);
+ const controller = effect.ac;
+
+ if (controller !== null) {
+ without_reactive_context(() => {
+ controller.abort(STALE_REACTION);
+ });
+ }
var next = effect.next;
diff --git a/packages/svelte/src/internal/client/reactivity/props.js b/packages/svelte/src/internal/client/reactivity/props.js
index 05b747a1c4..8353eb39e2 100644
--- a/packages/svelte/src/internal/client/reactivity/props.js
+++ b/packages/svelte/src/internal/client/reactivity/props.js
@@ -184,8 +184,7 @@ export function legacy_rest_props(props, exclude) {
* The proxy handler for spread props. Handles the incoming array of props
* that looks like `() => { dynamic: props }, { static: prop }, ..` and wraps
* them so that the whole thing is passed to the component as the `$$props` argument.
- * @template {Record} T
- * @type {ProxyHandler<{ props: Array T)> }>}}
+ * @type {ProxyHandler<{ props: Array | (() => Record)> }>}}
*/
const spread_props_handler = {
get(target, key) {
@@ -362,22 +361,23 @@ export function prop(props, key, flags, fallback) {
// means we can just call `$$props.foo = value` directly
if (setter) {
var legacy_parent = props.$$legacy;
-
- return function (/** @type {any} */ value, /** @type {boolean} */ mutation) {
- if (arguments.length > 0) {
- // We don't want to notify if the value was mutated and the parent is in runes mode.
- // In that case the state proxy (if it exists) should take care of the notification.
- // If the parent is not in runes mode, we need to notify on mutation, too, that the prop
- // has changed because the parent will not be able to detect the change otherwise.
- if (!runes || !mutation || legacy_parent || is_store_sub) {
- /** @type {Function} */ (setter)(mutation ? getter() : value);
+ return /** @type {() => V} */ (
+ function (/** @type {V} */ value, /** @type {boolean} */ mutation) {
+ if (arguments.length > 0) {
+ // We don't want to notify if the value was mutated and the parent is in runes mode.
+ // In that case the state proxy (if it exists) should take care of the notification.
+ // If the parent is not in runes mode, we need to notify on mutation, too, that the prop
+ // has changed because the parent will not be able to detect the change otherwise.
+ if (!runes || !mutation || legacy_parent || is_store_sub) {
+ /** @type {Function} */ (setter)(mutation ? getter() : value);
+ }
+
+ return value;
}
- return value;
+ return getter();
}
-
- return getter();
- };
+ );
}
// Either prop is written to, but there's no binding, which means we
@@ -400,29 +400,31 @@ export function prop(props, key, flags, fallback) {
var parent_effect = /** @type {Effect} */ (active_effect);
- return function (/** @type {any} */ value, /** @type {boolean} */ mutation) {
- if (arguments.length > 0) {
- const new_value = mutation ? get(d) : runes && bindable ? proxy(value) : value;
+ return /** @type {() => V} */ (
+ function (/** @type {any} */ value, /** @type {boolean} */ mutation) {
+ if (arguments.length > 0) {
+ const new_value = mutation ? get(d) : runes && bindable ? proxy(value) : value;
+
+ set(d, new_value);
+ overridden = true;
- set(d, new_value);
- overridden = true;
+ if (fallback_value !== undefined) {
+ fallback_value = new_value;
+ }
- if (fallback_value !== undefined) {
- fallback_value = new_value;
+ return value;
}
- return value;
- }
+ // special case — avoid recalculating the derived if we're in a
+ // teardown function and the prop was overridden locally, or the
+ // component was already destroyed (this latter part is necessary
+ // because `bind:this` can read props after the component has
+ // been destroyed. TODO simplify `bind:this`
+ if ((is_destroying_effect && overridden) || (parent_effect.f & DESTROYED) !== 0) {
+ return d.v;
+ }
- // special case — avoid recalculating the derived if we're in a
- // teardown function and the prop was overridden locally, or the
- // component was already destroyed (this latter part is necessary
- // because `bind:this` can read props after the component has
- // been destroyed. TODO simplify `bind:this`
- if ((is_destroying_effect && overridden) || (parent_effect.f & DESTROYED) !== 0) {
- return d.v;
+ return get(d);
}
-
- return get(d);
- };
+ );
}
diff --git a/packages/svelte/src/internal/client/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js
index 3b2087d56b..7fb3135708 100644
--- a/packages/svelte/src/internal/client/reactivity/sources.js
+++ b/packages/svelte/src/internal/client/reactivity/sources.js
@@ -37,6 +37,7 @@ import { Batch, schedule_effect } from './batch.js';
import { proxy } from '../proxy.js';
import { execute_derived } from './deriveds.js';
+/** @type {Set} */
export let inspect_effects = new Set();
/** @type {Map} */
diff --git a/packages/svelte/src/internal/client/render.js b/packages/svelte/src/internal/client/render.js
index ff6844453d..c5015875a8 100644
--- a/packages/svelte/src/internal/client/render.js
+++ b/packages/svelte/src/internal/client/render.js
@@ -136,20 +136,28 @@ export function hydrate(component, options) {
return /** @type {Exports} */ (instance);
} catch (error) {
- if (error === HYDRATION_ERROR) {
- if (options.recover === false) {
- e.hydration_failed();
- }
-
- // If an error occured above, the operations might not yet have been initialised.
- init_operations();
- clear_text_content(target);
+ // re-throw Svelte errors - they are certainly not related to hydration
+ if (
+ error instanceof Error &&
+ error.message.split('\n').some((line) => line.startsWith('https://svelte.dev/e/'))
+ ) {
+ throw error;
+ }
+ if (error !== HYDRATION_ERROR) {
+ // eslint-disable-next-line no-console
+ console.warn('Failed to hydrate: ', error);
+ }
- set_hydrating(false);
- return mount(component, options);
+ if (options.recover === false) {
+ e.hydration_failed();
}
- throw error;
+ // If an error occured above, the operations might not yet have been initialised.
+ init_operations();
+ clear_text_content(target);
+
+ set_hydrating(false);
+ return mount(component, options);
} finally {
set_hydrating(was_hydrating);
set_hydrate_node(previous_hydrate_node);
@@ -169,6 +177,7 @@ const document_listeners = new Map();
function _mount(Component, { target, anchor, props = {}, events, context, intro = true }) {
init_operations();
+ /** @type {Set} */
var registered_events = new Set();
/** @param {Array} events */
diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js
index e86866af2a..22a1890e0f 100644
--- a/packages/svelte/src/internal/client/runtime.js
+++ b/packages/svelte/src/internal/client/runtime.js
@@ -46,6 +46,7 @@ import { Batch, batch_deriveds, flushSync, schedule_effect } from './reactivity/
import { handle_error } from './error-handling.js';
import { UNINITIALIZED } from '../../constants.js';
import { captured_signals } from './legacy.js';
+import { without_reactive_context } from './dom/elements/bindings/shared.js';
export let is_updating_effect = false;
@@ -278,13 +279,17 @@ export function update_reaction(reaction) {
update_version = ++read_version;
if (reaction.ac !== null) {
- reaction.ac.abort(STALE_REACTION);
+ without_reactive_context(() => {
+ /** @type {AbortController} */ (reaction.ac).abort(STALE_REACTION);
+ });
+
reaction.ac = null;
}
try {
reaction.f |= REACTION_IS_UPDATING;
- var result = /** @type {Function} */ (0, reaction.fn)();
+ var fn = /** @type {Function} */ (reaction.fn);
+ var result = fn();
var deps = reaction.deps;
if (new_deps !== null) {
diff --git a/packages/svelte/src/internal/server/payload.js b/packages/svelte/src/internal/server/payload.js
index 195488e061..a7e40ad1db 100644
--- a/packages/svelte/src/internal/server/payload.js
+++ b/packages/svelte/src/internal/server/payload.js
@@ -6,7 +6,12 @@ export class HeadPayload {
uid = () => '';
title = '';
- constructor(css = new Set(), /** @type {string[]} */ out = [], title = '', uid = () => '') {
+ constructor(
+ /** @type {Set<{ hash: string; code: string }>} */ css = new Set(),
+ /** @type {string[]} */ out = [],
+ title = '',
+ uid = () => ''
+ ) {
this.css = css;
this.out = out;
this.title = title;
diff --git a/packages/svelte/src/internal/shared/validate.js b/packages/svelte/src/internal/shared/validate.js
index 8f3e2807e7..48e76f0958 100644
--- a/packages/svelte/src/internal/shared/validate.js
+++ b/packages/svelte/src/internal/shared/validate.js
@@ -35,7 +35,7 @@ export function validate_store(store, name) {
}
/**
- * @template {() => unknown} T
+ * @template {(...args: any[]) => unknown} T
* @param {T} fn
*/
export function prevent_snippet_stringification(fn) {
diff --git a/packages/svelte/src/utils.js b/packages/svelte/src/utils.js
index cd79cfc274..f8c39253ac 100644
--- a/packages/svelte/src/utils.js
+++ b/packages/svelte/src/utils.js
@@ -452,7 +452,7 @@ const RUNES = /** @type {const} */ ([
'$host'
]);
-/** @typedef {RUNES[number]} RuneName */
+/** @typedef {typeof RUNES[number]} RuneName */
/**
* @param {string} name
@@ -462,7 +462,7 @@ export function is_rune(name) {
return RUNES.includes(/** @type {RuneName} */ (name));
}
-/** @typedef {STATE_CREATION_RUNES[number]} StateCreationRuneName */
+/** @typedef {typeof STATE_CREATION_RUNES[number]} StateCreationRuneName */
/**
* @param {string} name
@@ -477,7 +477,7 @@ const RAW_TEXT_ELEMENTS = /** @type {const} */ (['textarea', 'script', 'style',
/** @param {string} name */
export function is_raw_text_element(name) {
- return RAW_TEXT_ELEMENTS.includes(/** @type {RAW_TEXT_ELEMENTS[number]} */ (name));
+ return RAW_TEXT_ELEMENTS.includes(/** @type {typeof RAW_TEXT_ELEMENTS[number]} */ (name));
}
/**
diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js
index e83ba6fb30..cf71ae709a 100644
--- a/packages/svelte/src/version.js
+++ b/packages/svelte/src/version.js
@@ -4,5 +4,5 @@
* The current version, as set in package.json.
* @type {string}
*/
-export const VERSION = '5.37.0';
+export const VERSION = '5.38.1';
export const PUBLIC_VERSION = '5';
diff --git a/packages/svelte/tests/animation-helpers.js b/packages/svelte/tests/animation-helpers.js
index dcbb062923..0452a0d643 100644
--- a/packages/svelte/tests/animation-helpers.js
+++ b/packages/svelte/tests/animation-helpers.js
@@ -3,7 +3,9 @@ import { raf as svelte_raf } from 'svelte/internal/client';
import { queue_micro_task } from '../src/internal/client/dom/task.js';
export const raf = {
+ /** @type {Set} */
animations: new Set(),
+ /** @type {Set<(n: number) => void>} */
ticks: new Set(),
tick,
time: 0,
@@ -54,14 +56,24 @@ class Animation {
/**
* @param {HTMLElement} target
- * @param {Keyframe[]} keyframes
- * @param {{ duration: number, delay: number }} options
+ * @param {Keyframe[] | PropertyIndexedKeyframes | null} keyframes
+ * @param {number | KeyframeAnimationOptions | undefined} options
*/
- constructor(target, keyframes, { duration, delay }) {
+ constructor(target, keyframes, options) {
this.target = target;
- this.#keyframes = keyframes;
- this.#duration = Math.round(duration);
- this.#delay = delay ?? 0;
+ this.#keyframes = Array.isArray(keyframes) ? keyframes : [];
+ if (typeof options === 'number') {
+ this.#duration = options;
+ this.#delay = 0;
+ } else {
+ const { duration = 0, delay = 0 } = options ?? {};
+ if (typeof duration === 'object') {
+ this.#duration = 0;
+ } else {
+ this.#duration = Math.round(+duration);
+ }
+ this.#delay = delay;
+ }
this._update();
}
@@ -189,6 +201,7 @@ function interpolate(a, b, p) {
* @param {{duration: number, delay: number}} options
* @returns {globalThis.Animation}
*/
+// @ts-ignore
HTMLElement.prototype.animate = function (keyframes, options) {
const animation = new Animation(this, keyframes, options);
raf.animations.add(animation);
@@ -196,6 +209,7 @@ HTMLElement.prototype.animate = function (keyframes, options) {
return animation;
};
+// @ts-ignore
HTMLElement.prototype.getAnimations = function () {
return Array.from(raf.animations).filter((animation) => animation.target === this);
};
diff --git a/packages/svelte/tests/compiler-errors/samples/snippet-invalid-export/_config.js b/packages/svelte/tests/compiler-errors/samples/snippet-invalid-export/_config.js
new file mode 100644
index 0000000000..cc0dd388f4
--- /dev/null
+++ b/packages/svelte/tests/compiler-errors/samples/snippet-invalid-export/_config.js
@@ -0,0 +1,10 @@
+import { test } from '../../test';
+
+export default test({
+ error: {
+ code: 'snippet_invalid_export',
+ message:
+ 'An exported snippet can only reference things declared in a `
+
+
+
+{#snippet foo()}
+ {x}
+{/snippet}
diff --git a/packages/svelte/tests/helpers.js b/packages/svelte/tests/helpers.js
index 410838829e..7a9640636c 100644
--- a/packages/svelte/tests/helpers.js
+++ b/packages/svelte/tests/helpers.js
@@ -43,6 +43,7 @@ export function create_deferred() {
/** @param {any} [reason] */
let reject = (reason) => {};
+ /** @type {Promise} */
const promise = new Promise((f, r) => {
resolve = f;
reject = r;
diff --git a/packages/svelte/tests/hydration/samples/whitespace-at-block-start/Nested.svelte b/packages/svelte/tests/hydration/samples/whitespace-at-block-start/Nested.svelte
new file mode 100644
index 0000000000..70bf63ad9d
--- /dev/null
+++ b/packages/svelte/tests/hydration/samples/whitespace-at-block-start/Nested.svelte
@@ -0,0 +1 @@
+
nested
\ No newline at end of file
diff --git a/packages/svelte/tests/hydration/samples/whitespace-at-block-start/_config.js b/packages/svelte/tests/hydration/samples/whitespace-at-block-start/_config.js
new file mode 100644
index 0000000000..457eeb2201
--- /dev/null
+++ b/packages/svelte/tests/hydration/samples/whitespace-at-block-start/_config.js
@@ -0,0 +1,8 @@
+import { test } from '../../test';
+
+export default test({
+ errors: [
+ 'Failed to hydrate: ',
+ new DOMException("Node can't be inserted in a #text parent.", 'HierarchyRequestError')
+ ]
+});
diff --git a/packages/svelte/tests/hydration/samples/whitespace-at-block-start/_expected.html b/packages/svelte/tests/hydration/samples/whitespace-at-block-start/_expected.html
new file mode 100644
index 0000000000..46f8e8a7ac
--- /dev/null
+++ b/packages/svelte/tests/hydration/samples/whitespace-at-block-start/_expected.html
@@ -0,0 +1 @@
+
nested
\ No newline at end of file
diff --git a/packages/svelte/tests/hydration/samples/whitespace-at-block-start/_override.html b/packages/svelte/tests/hydration/samples/whitespace-at-block-start/_override.html
new file mode 100644
index 0000000000..90ca4ef4b8
--- /dev/null
+++ b/packages/svelte/tests/hydration/samples/whitespace-at-block-start/_override.html
@@ -0,0 +1,2 @@
+
+
');
+
+ assert.equal(
+ warnings[0],
+ 'Detected reactivity loss when reading `values[1]`. 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-for-await/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss-for-await/main.svelte
new file mode 100644
index 0000000000..92a6ec18bc
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss-for-await/main.svelte
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
{await get_total()}
+
+ {#snippet pending()}
+
pending
+ {/snippet}
+
diff --git a/packages/svelte/tests/runtime-runes/samples/svelte-component-props-update/Comp-1.svelte b/packages/svelte/tests/runtime-runes/samples/svelte-component-props-update/Comp-1.svelte
new file mode 100644
index 0000000000..fdafa27c3c
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/svelte-component-props-update/Comp-1.svelte
@@ -0,0 +1,7 @@
+
+
+{#each data.obj.arr as i}
+
{i}
+{/each}
\ No newline at end of file
diff --git a/packages/svelte/tests/runtime-runes/samples/svelte-component-props-update/Comp-2.svelte b/packages/svelte/tests/runtime-runes/samples/svelte-component-props-update/Comp-2.svelte
new file mode 100644
index 0000000000..e345a7697c
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/svelte-component-props-update/Comp-2.svelte
@@ -0,0 +1 @@
+
Comp 2
\ No newline at end of file
diff --git a/packages/svelte/tests/runtime-runes/samples/svelte-component-props-update/_config.js b/packages/svelte/tests/runtime-runes/samples/svelte-component-props-update/_config.js
new file mode 100644
index 0000000000..ff5ca12dbf
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/svelte-component-props-update/_config.js
@@ -0,0 +1,11 @@
+import { flushSync } from 'svelte';
+import { test } from '../../test';
+
+export default test({
+ async test({ assert, target }) {
+ const [btn] = target.querySelectorAll('button');
+ btn.click();
+ flushSync();
+ assert.htmlEqual(target.innerHTML, `
Comp 2
`);
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/svelte-component-props-update/main.svelte b/packages/svelte/tests/runtime-runes/samples/svelte-component-props-update/main.svelte
new file mode 100644
index 0000000000..abd785fff3
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/svelte-component-props-update/main.svelte
@@ -0,0 +1,16 @@
+
+
+
+
+
diff --git a/packages/svelte/tests/validator/samples/class-state-constructor-9/input.svelte.js b/packages/svelte/tests/validator/samples/class-state-constructor-9/input.svelte.js
index a8469e13af..3806046f3f 100644
--- a/packages/svelte/tests/validator/samples/class-state-constructor-9/input.svelte.js
+++ b/packages/svelte/tests/validator/samples/class-state-constructor-9/input.svelte.js
@@ -1,6 +1,6 @@
export class Counter {
count = -1;
-
+ static count() {}
constructor() {
this.count = $state(0);
}
diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts
index 9ea45af7e6..97e6f0f5a3 100644
--- a/packages/svelte/types/index.d.ts
+++ b/packages/svelte/types/index.d.ts
@@ -1296,7 +1296,17 @@ declare module 'svelte/compiler' {
name: string;
/** The 'y' in `on:x={y}` */
expression: null | Expression;
- modifiers: string[];
+ modifiers: Array<
+ | 'capture'
+ | 'nonpassive'
+ | 'once'
+ | 'passive'
+ | 'preventDefault'
+ | 'self'
+ | 'stopImmediatePropagation'
+ | 'stopPropagation'
+ | 'trusted'
+ >;
}
/** A `style:` directive */
@@ -3303,7 +3313,7 @@ declare namespace $derived {
*
* If you return a function from the effect, it will be called right before the effect is run again, or when the component is unmounted.
*
- * Does not run during server side rendering.
+ * Does not run during server-side rendering.
*
* https://svelte.dev/docs/svelte/$effect
* @param fn The function to execute
@@ -3322,7 +3332,7 @@ declare namespace $effect {
*
* If you return a function from the effect, it will be called right before the effect is run again, or when the component is unmounted.
*
- * Does not run during server side rendering.
+ * Does not run during server-side rendering.
*
* https://svelte.dev/docs/svelte/$effect#$effect.pre
* @param fn The function to execute
diff --git a/playgrounds/sandbox/package.json b/playgrounds/sandbox/package.json
index 3ab65ac4b5..f11da983f6 100644
--- a/playgrounds/sandbox/package.json
+++ b/playgrounds/sandbox/package.json
@@ -11,7 +11,8 @@
"prod": "npm run build && node dist/server/ssr-prod",
"preview": "vite preview",
"download": "node scripts/download.js",
- "hash": "node scripts/hash.js"
+ "hash": "node scripts/hash.js",
+ "create-test": "node scripts/create-test.js"
},
"devDependencies": {
"@sveltejs/vite-plugin-svelte": "^4.0.0-next.6",
diff --git a/playgrounds/sandbox/scripts/create-test.js b/playgrounds/sandbox/scripts/create-test.js
new file mode 100644
index 0000000000..c733f19419
--- /dev/null
+++ b/playgrounds/sandbox/scripts/create-test.js
@@ -0,0 +1,155 @@
+// Creates a test from the existing playground. cwd needs to be playground/sandbox
+import fs from 'fs';
+import path from 'path';
+
+// Get target folder from command line arguments
+let target_folder = process.argv[2];
+if (!target_folder) {
+ console.error(
+ 'Please provide a target folder as an argument. Example: node create-test.js runtime-runes/my-test'
+ );
+ process.exit(1);
+}
+if (!target_folder.includes('/')) {
+ target_folder = 'runtime-runes/' + target_folder;
+}
+if (!target_folder.startsWith('runtime-')) {
+ console.error(
+ 'Target folder must start with "runtime-" (can only convert to these kinds of tests)'
+ );
+ process.exit(1);
+}
+target_folder = path.join(
+ path.resolve('../../packages/svelte/tests', target_folder.split('/')[0]),
+ 'samples',
+ target_folder.split('/')[1]
+);
+
+const exists = fs.existsSync(target_folder);
+
+// Check if target folder already exists and ask for confirmation
+if (exists) {
+ console.log(`Target folder "${target_folder}" already exists.`);
+ process.stdout.write('Do you want to override the existing test? (Y/n): ');
+
+ // Read user input synchronously
+ const stdin = process.stdin;
+ stdin.setRawMode(true);
+ stdin.resume();
+ stdin.setEncoding('utf8');
+
+ const response = await new Promise((resolve) => {
+ stdin.on('data', (key) => {
+ stdin.setRawMode(false);
+ stdin.pause();
+ process.stdout.write('\n');
+ resolve(key);
+ });
+ });
+
+ if (response.toLowerCase() === 'n') {
+ console.log('Operation cancelled.');
+ process.exit(0);
+ }
+
+ // Clear the existing target folder except for _config.js
+ const files = fs.readdirSync(target_folder);
+ for (const file of files) {
+ if (file !== '_config.js') {
+ const filePath = path.join(target_folder, file);
+ fs.rmSync(filePath, { recursive: true, force: true });
+ }
+ }
+} else {
+ fs.mkdirSync(target_folder, { recursive: true });
+}
+
+// Starting file
+const app_svelte_path = path.resolve('./src/App.svelte');
+const collected_files = new Set();
+const processed_files = new Set();
+
+function collect_imports(file_path) {
+ if (processed_files.has(file_path) || !fs.existsSync(file_path)) {
+ return;
+ }
+
+ processed_files.add(file_path);
+ collected_files.add(file_path);
+
+ const content = fs.readFileSync(file_path, 'utf8');
+
+ // Regex to match import statements
+ const import_regex = /import\s+(?:[^'"]*\s+from\s+)?['"]([^'"]+)['"]/g;
+ let match;
+
+ while ((match = import_regex.exec(content)) !== null) {
+ const import_path = match[1];
+
+ // Skip node_modules imports
+ if (!import_path.startsWith('.')) {
+ continue;
+ }
+
+ // Resolve relative import path
+ const resolved_path = path.resolve(path.dirname(file_path), import_path);
+
+ // Try different extensions if file doesn't exist
+ const extensions = ['', '.svelte', '.js', '.ts'];
+ let actual_path = null;
+
+ for (const ext of extensions) {
+ const test_path = resolved_path + ext;
+ if (fs.existsSync(test_path)) {
+ actual_path = test_path;
+ break;
+ }
+ }
+
+ if (actual_path) {
+ collect_imports(actual_path);
+ }
+ }
+}
+
+// Start collecting from App.svelte
+collect_imports(app_svelte_path);
+
+// Copy collected files to target folder
+for (const file_path of collected_files) {
+ const relative_path = path.relative(path.resolve('./src'), file_path);
+ let target_path = path.join(target_folder, relative_path);
+
+ // Rename App.svelte to main.svelte
+ if (path.basename(file_path) === 'App.svelte') {
+ target_path = path.join(target_folder, path.dirname(relative_path), 'main.svelte');
+ }
+
+ // Ensure target directory exists
+ const target_dir = path.dirname(target_path);
+ if (!fs.existsSync(target_dir)) {
+ fs.mkdirSync(target_dir, { recursive: true });
+ }
+
+ // Copy file
+ fs.copyFileSync(file_path, target_path);
+ console.log(`Copied: ${file_path} -> ${target_path}`);
+}
+
+// Create empty _config.js
+if (!exists) {
+ const config_path = path.join(target_folder, '_config.js');
+ fs.writeFileSync(
+ config_path,
+ `import { test } from '../../test';
+
+export default test({
+ async test({ assert, target }) {
+ }
+});
+`
+ );
+ console.log(`Created: ${config_path}`);
+}
+
+console.log(`\nTest files created in: ${target_folder}`);
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 315d699e25..cad5fefdf8 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -62,9 +62,9 @@ importers:
packages/svelte:
dependencies:
- '@ampproject/remapping':
- specifier: ^2.3.0
- version: 2.3.0
+ '@jridgewell/remapping':
+ specifier: ^2.3.4
+ version: 2.3.4
'@jridgewell/sourcemap-codec':
specifier: ^1.5.0
version: 1.5.0
@@ -457,6 +457,9 @@ packages:
resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==}
engines: {node: '>=6.0.0'}
+ '@jridgewell/remapping@2.3.4':
+ resolution: {integrity: sha512-aG+WvAz17rhbzhKNkSeMLgbkPPK82ovXdONvmucbGhUqcroRFLLVhoGAk4xEI17gHpXgNX3sr0/B1ybRUsbEWw==}
+
'@jridgewell/resolve-uri@3.1.1':
resolution: {integrity: sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==}
engines: {node: '>=6.0.0'}
@@ -2777,6 +2780,11 @@ snapshots:
'@jridgewell/sourcemap-codec': 1.5.0
'@jridgewell/trace-mapping': 0.3.25
+ '@jridgewell/remapping@2.3.4':
+ dependencies:
+ '@jridgewell/gen-mapping': 0.3.5
+ '@jridgewell/trace-mapping': 0.3.25
+
'@jridgewell/resolve-uri@3.1.1': {}
'@jridgewell/set-array@1.2.1': {}