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/CONTRIBUTING.md b/CONTRIBUTING.md index c2d3e45049..0653b08b76 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -101,13 +101,13 @@ Test samples are kept in `/test/xxx/samples` folder. 1. To run test, run `pnpm test`. 1. To run a particular test suite, use `pnpm test `, for example: - ```bash + ```sh pnpm test validator ``` 1. To filter tests _within_ a test suite, use `pnpm test -t `, for example: - ```bash + ```sh pnpm test validator -t a11y-alt-text ``` diff --git a/documentation/docs/01-introduction/02-getting-started.md b/documentation/docs/01-introduction/02-getting-started.md index c7351729ff..e97a46ad34 100644 --- a/documentation/docs/01-introduction/02-getting-started.md +++ b/documentation/docs/01-introduction/02-getting-started.md @@ -4,7 +4,7 @@ title: Getting started We recommend using [SvelteKit](../kit), which lets you [build almost anything](../kit/project-types). It's the official application framework from the Svelte team and powered by [Vite](https://vite.dev/). Create a new project with: -```bash +```sh npx sv create myapp cd myapp npm install 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/02-testing.md b/documentation/docs/07-misc/02-testing.md index db99b70770..bcec4db0a3 100644 --- a/documentation/docs/07-misc/02-testing.md +++ b/documentation/docs/07-misc/02-testing.md @@ -10,7 +10,7 @@ Unit tests allow you to test small isolated parts of your code. Integration test To setup Vitest manually, first install it: -```bash +```sh npm install -D vitest ``` @@ -166,7 +166,7 @@ It is possible to test your components in isolation using Vitest. To get started, install jsdom (a library that shims DOM APIs): -```bash +```sh npm install -D jsdom ``` 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-errors.md b/documentation/docs/98-reference/.generated/compile-errors.md index 20f57770d1..b9c44163c9 100644 --- a/documentation/docs/98-reference/.generated/compile-errors.md +++ b/documentation/docs/98-reference/.generated/compile-errors.md @@ -196,6 +196,51 @@ Cyclical dependency detected: %cycle% `{@const}` must be the immediate child of `{#snippet}`, `{#if}`, `{:else if}`, `{:else}`, `{#each}`, `{:then}`, `{:catch}`, ``, `` ``` +### const_tag_invalid_reference + +``` +The `{@const %name% = ...}` declaration is not available in this snippet +``` + +The following is an error: + +```svelte + + {@const foo = 'bar'} + + {#snippet failed()} + {foo} + {/snippet} + +``` + +Here, `foo` is not available inside `failed`. The top level code inside `` becomes part of the implicit `children` snippet, in other words the above code is equivalent to this: + +```svelte + + {#snippet children()} + {@const foo = 'bar'} + {/snippet} + + {#snippet failed()} + {foo} + {/snippet} + +``` + +The same applies to components: + +```svelte + + {@const foo = 'bar'} + + {#snippet someProp()} + + {foo} + {/snippet} + +``` + ### constant_assignment ``` @@ -364,6 +409,12 @@ The $ name is reserved, and cannot be used for variables and imports The $ prefix is reserved, and cannot be used for variables and imports ``` +### duplicate_class_field + +``` +`%name%` has already been declared +``` + ### each_item_invalid_assignment ``` diff --git a/documentation/docs/98-reference/.generated/compile-warnings.md b/documentation/docs/98-reference/.generated/compile-warnings.md index 2af9021a6a..17841b863c 100644 --- a/documentation/docs/98-reference/.generated/compile-warnings.md +++ b/documentation/docs/98-reference/.generated/compile-warnings.md @@ -679,11 +679,11 @@ 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: -```bash +```sh npx sv migrate self-closing-tags ``` 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 5a5e532a08..fb6b20c489 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,129 @@ # svelte +## 5.38.3 + +### Patch Changes + +- fix: ensure correct order of template effect values ([#16655](https://github.com/sveltejs/svelte/pull/16655)) + +- fix: allow async `{@const}` in more places ([#16643](https://github.com/sveltejs/svelte/pull/16643)) + +- fix: properly catch top level await errors ([#16619](https://github.com/sveltejs/svelte/pull/16619)) + +- perf: prune effects without dependencies ([#16625](https://github.com/sveltejs/svelte/pull/16625)) + +- fix: only emit `for_await_track_reactivity_loss` in async mode ([#16644](https://github.com/sveltejs/svelte/pull/16644)) + +## 5.38.2 + +### Patch Changes + +- perf: run blocks eagerly during flush instead of aborting ([#16631](https://github.com/sveltejs/svelte/pull/16631)) + +- fix: don't clone non-proxies in `$inspect` ([#16617](https://github.com/sveltejs/svelte/pull/16617)) + +- fix: avoid recursion error when tagging circular references ([#16622](https://github.com/sveltejs/svelte/pull/16622)) + +## 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 + +- feat: ignore component options in `compileModule` ([#16362](https://github.com/sveltejs/svelte/pull/16362)) + +### Patch Changes + +- fix: always mark props as stateful ([#16504](https://github.com/sveltejs/svelte/pull/16504)) + +## 5.36.17 + +### Patch Changes + +- fix: throw on duplicate class field declarations ([#16502](https://github.com/sveltejs/svelte/pull/16502)) + +- fix: add types for `part` attribute to svg attributes ([#16499](https://github.com/sveltejs/svelte/pull/16499)) + +## 5.36.16 + +### Patch Changes + +- fix: don't update a focused input with values from its own past ([#16491](https://github.com/sveltejs/svelte/pull/16491)) + +- fix: don't destroy effect roots created inside of deriveds ([#16492](https://github.com/sveltejs/svelte/pull/16492)) + +## 5.36.15 + +### Patch Changes + +- fix: preserve dirty status of deferred effects ([#16487](https://github.com/sveltejs/svelte/pull/16487)) + +## 5.36.14 + +### Patch Changes + +- fix: keep input in sync when binding updated via effect ([#16482](https://github.com/sveltejs/svelte/pull/16482)) + +- fix: rename form accept-charset attribute ([#16478](https://github.com/sveltejs/svelte/pull/16478)) + +- fix: prevent infinite async loop ([#16482](https://github.com/sveltejs/svelte/pull/16482)) + +- fix: exclude derived writes from effect abort and rescheduling ([#16482](https://github.com/sveltejs/svelte/pull/16482)) + ## 5.36.13 ### Patch Changes diff --git a/packages/svelte/README.md b/packages/svelte/README.md index c37617da30..026bc0d81d 100644 --- a/packages/svelte/README.md +++ b/packages/svelte/README.md @@ -19,7 +19,7 @@ You can play around with Svelte in the [tutorial](https://svelte.dev/tutorial), When you're ready to build a full-fledge application, we recommend using [SvelteKit](https://svelte.dev/docs/kit): -```bash +```sh npx sv create my-app cd my-app npm install @@ -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 1492f77792..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; @@ -996,7 +998,7 @@ export interface HTMLFieldsetAttributes extends HTMLAttributes { - acceptcharset?: string | undefined | null; + 'accept-charset'?: 'utf-8' | (string & {}) | undefined | null; action?: string | undefined | null; autocomplete?: AutoFillBase | undefined | null; enctype?: @@ -1553,6 +1555,7 @@ export interface SVGAttributes extends AriaAttributes, DO height?: number | string | undefined | null; id?: string | undefined | null; lang?: string | undefined | null; + part?: string | undefined | null; max?: number | string | undefined | null; media?: string | undefined | null; // On the `textPath` element diff --git a/packages/svelte/messages/compile-errors/script.md b/packages/svelte/messages/compile-errors/script.md index 2b0c5eafdf..5c1080aced 100644 --- a/packages/svelte/messages/compile-errors/script.md +++ b/packages/svelte/messages/compile-errors/script.md @@ -30,6 +30,10 @@ > The $ prefix is reserved, and cannot be used for variables and imports +## duplicate_class_field + +> `%name%` has already been declared + ## each_item_invalid_assignment > Cannot reassign or bind to each block argument in runes mode. Use the array and index variables instead (e.g. `array[i] = value` instead of `entry = value`, or `bind:value={array[i]}` instead of `bind:value={entry}`) diff --git a/packages/svelte/messages/compile-errors/template.md b/packages/svelte/messages/compile-errors/template.md index 0569f63ad3..dc26a02767 100644 --- a/packages/svelte/messages/compile-errors/template.md +++ b/packages/svelte/messages/compile-errors/template.md @@ -124,6 +124,49 @@ > `{@const}` must be the immediate child of `{#snippet}`, `{#if}`, `{:else if}`, `{:else}`, `{#each}`, `{:then}`, `{:catch}`, ``, `` +## const_tag_invalid_reference + +> The `{@const %name% = ...}` declaration is not available in this snippet + +The following is an error: + +```svelte + + {@const foo = 'bar'} + + {#snippet failed()} + {foo} + {/snippet} + +``` + +Here, `foo` is not available inside `failed`. The top level code inside `` becomes part of the implicit `children` snippet, in other words the above code is equivalent to this: + +```svelte + + {#snippet children()} + {@const foo = 'bar'} + {/snippet} + + {#snippet failed()} + {foo} + {/snippet} + +``` + +The same applies to components: + +```svelte + + {@const foo = 'bar'} + + {#snippet someProp()} + + {foo} + {/snippet} + +``` + ## debug_tag_invalid_arguments > {@debug ...} arguments must be identifiers, not arbitrary expressions diff --git a/packages/svelte/messages/compile-warnings/template.md b/packages/svelte/messages/compile-warnings/template.md index d61a61d950..3650e07b47 100644 --- a/packages/svelte/messages/compile-warnings/template.md +++ b/packages/svelte/messages/compile-warnings/template.md @@ -67,11 +67,11 @@ 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: -```bash +```sh npx sv migrate self-closing-tags ``` diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 4bf9a5df22..b7effe35bd 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.36.13", + "version": "5.38.3", "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/errors.js b/packages/svelte/src/compiler/errors.js index 599d3e8248..44fc641ee5 100644 --- a/packages/svelte/src/compiler/errors.js +++ b/packages/svelte/src/compiler/errors.js @@ -152,6 +152,16 @@ export function dollar_prefix_invalid(node) { e(node, 'dollar_prefix_invalid', `The $ prefix is reserved, and cannot be used for variables and imports\nhttps://svelte.dev/e/dollar_prefix_invalid`); } +/** + * `%name%` has already been declared + * @param {null | number | NodeLike} node + * @param {string} name + * @returns {never} + */ +export function duplicate_class_field(node, name) { + e(node, 'duplicate_class_field', `\`${name}\` has already been declared\nhttps://svelte.dev/e/duplicate_class_field`); +} + /** * Cannot reassign or bind to each block argument in runes mode. Use the array and index variables instead (e.g. `array[i] = value` instead of `entry = value`, or `bind:value={array[i]}` instead of `bind:value={entry}`) * @param {null | number | NodeLike} node @@ -975,6 +985,16 @@ export function const_tag_invalid_placement(node) { e(node, 'const_tag_invalid_placement', `\`{@const}\` must be the immediate child of \`{#snippet}\`, \`{#if}\`, \`{:else if}\`, \`{:else}\`, \`{#each}\`, \`{:then}\`, \`{:catch}\`, \`\`, \`\`\nhttps://svelte.dev/e/const_tag_invalid_placement`); } +/** + * The `{@const %name% = ...}` declaration is not available in this snippet + * @param {null | number | NodeLike} node + * @param {string} name + * @returns {never} + */ +export function const_tag_invalid_reference(node, name) { + e(node, 'const_tag_invalid_reference', `The \`{@const ${name} = ...}\` declaration is not available in this snippet \nhttps://svelte.dev/e/const_tag_invalid_reference`); +} + /** * {@debug ...} arguments must be identifiers, not arbitrary expressions * @param {null | number | NodeLike} node diff --git a/packages/svelte/src/compiler/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 77cc2bf3fa..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'; @@ -23,12 +22,6 @@ export class Parser { */ template; - /** - * @readonly - * @type {string} - */ - template_untrimmed; - /** * Whether or not we're in loose parsing mode, in which * case we try to continue parsing as much as possible @@ -67,7 +60,6 @@ export class Parser { } this.loose = loose; - this.template_untrimmed = template; this.template = template.trimEnd(); let match_lang; diff --git a/packages/svelte/src/compiler/phases/1-parse/state/element.js b/packages/svelte/src/compiler/phases/1-parse/state/element.js index 87332f647d..ed1b047d55 100644 --- a/packages/svelte/src/compiler/phases/1-parse/state/element.js +++ b/packages/svelte/src/compiler/phases/1-parse/state/element.js @@ -370,14 +370,6 @@ export default function element(parser) { // ... or we're followed by whitespace, for example near the end of the template, // which we want to take in so that language tools has more room to work with parser.allow_whitespace(); - if (parser.index === parser.template.length) { - while ( - parser.index < parser.template_untrimmed.length && - regex_whitespace.test(parser.template_untrimmed[parser.index]) - ) { - parser.index++; - } - } } } } 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 d407b44556..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 }, @@ -451,6 +454,8 @@ export function analyze_component(root, source, options) { } } + const is_custom_element = !!options.customElementOptions || options.customElement; + // TODO remove all the ?? stuff, we don't need it now that we're validating the config /** @type {ComponentAnalysis} */ const analysis = { @@ -500,13 +505,13 @@ export function analyze_component(root, source, options) { needs_props: false, event_directive_node: null, uses_event_attributes: false, - custom_element: options.customElementOptions ?? options.customElement, - inject_styles: options.css === 'injected' || options.customElement, - accessors: options.customElement - ? true - : (runes ? false : !!options.accessors) || - // because $set method needs accessors - options.compatibility?.componentApi === 4, + custom_element: is_custom_element, + inject_styles: options.css === 'injected' || is_custom_element, + accessors: + is_custom_element || + (runes ? false : !!options.accessors) || + // because $set method needs accessors + options.compatibility?.componentApi === 4, reactive_statements: new Map(), binding_groups: new Map(), slot_names: new Map(), @@ -524,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() @@ -686,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(), @@ -751,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', @@ -785,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 ffc39ac00d..dd21637174 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/ClassBody.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/ClassBody.js @@ -33,6 +33,9 @@ export function ClassBody(node, context) { /** @type {Map} */ const state_fields = new Map(); + /** @type {Map>} */ + const fields = new Map(); + context.state.analysis.classes.set(node, state_fields); /** @type {MethodDefinition | null} */ @@ -54,6 +57,14 @@ export function ClassBody(node, context) { e.state_field_duplicate(node, name); } + const _key = (node.type === 'AssignmentExpression' || !node.static ? '' : '@') + name; + const field = fields.get(_key); + + // if there's already a method or assigned field, error + if (field && !(field.length === 1 && field[0] === 'prop')) { + e.duplicate_class_field(node, _key); + } + state_fields.set(name, { node, type: rune, @@ -67,10 +78,48 @@ 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 = /** @type {string} */ (get_name(child.key)); + const field = fields.get(key); + if (!field) { + fields.set(key, [child.value ? 'assigned_prop' : 'prop']); + continue; + } + e.duplicate_class_field(child, key); } - if (child.type === 'MethodDefinition' && child.kind === 'constructor') { - constructor = child; + if (child.type === 'MethodDefinition') { + if (child.kind === 'constructor') { + constructor = child; + } else if (!child.computed) { + const key = (child.static ? '@' : '') + get_name(child.key); + const field = fields.get(key); + if (!field) { + fields.set(key, [child.kind]); + continue; + } + if ( + field.includes(child.kind) || + field.includes('prop') || + field.includes('assigned_prop') + ) { + e.duplicate_class_field(child, key); + } + if (child.kind === 'get') { + if (field.length === 1 && field[0] === 'set') { + field.push('get'); + continue; + } + } else if (child.kind === 'set') { + if (field.length === 1 && field[0] === 'get') { + field.push('set'); + continue; + } + } else { + field.push(child.kind); + continue; + } + e.duplicate_class_field(child, key); + } } } 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/Identifier.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/Identifier.js index cced326f9b..1c98a95e63 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/Identifier.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/Identifier.js @@ -7,6 +7,7 @@ import * as w from '../../../warnings.js'; import { is_rune } from '../../../../utils.js'; import { mark_subtree_dynamic } from './shared/fragment.js'; import { get_rune } from '../../scope.js'; +import { is_component_node } from '../../nodes.js'; /** * @param {Identifier} node @@ -93,7 +94,10 @@ export function Identifier(node, context) { context.state.expression.references.add(binding); context.state.expression.has_state ||= binding.kind !== 'static' && - !binding.is_function() && + (binding.kind === 'prop' || + binding.kind === 'bindable_prop' || + binding.kind === 'rest_prop' || + !binding.is_function()) && !context.state.scope.evaluate(node).is_known; } @@ -152,5 +156,37 @@ export function Identifier(node, context) { ) { w.reactive_declaration_module_script_dependency(node); } + + if (binding.metadata?.is_template_declaration && context.state.options.experimental.async) { + let snippet_name; + + // Find out if this references a {@const ...} declaration of an implicit children snippet + // when it is itself inside a snippet block at the same level. If so, error. + for (let i = context.path.length - 1; i >= 0; i--) { + const parent = context.path[i]; + const grand_parent = context.path[i - 1]; + + if (parent.type === 'SnippetBlock') { + snippet_name = parent.expression.name; + } else if ( + snippet_name && + grand_parent && + parent.type === 'Fragment' && + (is_component_node(grand_parent) || + (grand_parent.type === 'SvelteBoundary' && + (snippet_name === 'failed' || snippet_name === 'pending'))) + ) { + if ( + is_component_node(grand_parent) + ? grand_parent.metadata.scopes.default === binding.scope + : context.state.scopes.get(parent) === binding.scope + ) { + e.const_tag_invalid_reference(node, node.name); + } else { + break; + } + } + } + } } } diff --git a/packages/svelte/src/compiler/phases/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 124438a9da..940d6a9e00 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), @@ -356,16 +359,31 @@ export function client_component(analysis, options) { if (dev) push_args.push(b.id(analysis.name)); let component_block = b.block([ + store_init, ...store_setup, ...legacy_reactive_declarations, ...group_binding_declarations, - ...state.instance_level_snippets, - .../** @type {ESTree.Statement[]} */ (instance.body), - analysis.runes || !analysis.needs_context - ? b.empty - : b.stmt(b.call('$.init', analysis.immutable ? b.true : undefined)) + ...state.instance_level_snippets ]); + if (analysis.instance.has_await) { + const body = b.block([ + .../** @type {ESTree.Statement[]} */ (instance.body), + b.if(b.call('$.aborted'), b.return()), + .../** @type {ESTree.Statement[]} */ (template.body) + ]); + + component_block.body.push(b.stmt(b.call(`$.async_body`, b.arrow([], body, true)))); + } else { + component_block.body.push(.../** @type {ESTree.Statement[]} */ (instance.body)); + + if (!analysis.runes && analysis.needs_context) { + component_block.body.push(b.stmt(b.call('$.init', analysis.immutable ? b.true : undefined))); + } + + component_block.body.push(.../** @type {ESTree.Statement[]} */ (template.body)); + } + if (analysis.needs_mutation_validation) { component_block.body.unshift( b.var('$$ownership_validator', b.call('$.create_ownership_validator', b.id('$$props'))) @@ -386,41 +404,6 @@ export function client_component(analysis, options) { analysis.uses_slots || analysis.slot_names.size > 0; - if (analysis.instance.has_await) { - const params = [b.id('$$anchor')]; - if (should_inject_props) { - params.push(b.id('$$props')); - } - if (store_setup.length > 0) { - params.push(b.id('$$stores')); - } - const body = b.function_declaration( - b.id('$$body'), - params, - b.block([ - b.var('$$unsuspend', b.call('$.suspend')), - ...component_block.body, - b.if(b.call('$.aborted'), b.return()), - .../** @type {ESTree.Statement[]} */ (template.body), - b.stmt(b.call('$$unsuspend')) - ]), - true - ); - - state.hoisted.push(body); - - component_block = b.block([ - b.var('fragment', b.call('$.comment')), - b.var('node', b.call('$.first_child', b.id('fragment'))), - store_init, - b.stmt(b.call(body.id, b.id('node'), ...params.slice(1))), - b.stmt(b.call('$.append', b.id('$$anchor'), b.id('fragment'))) - ]); - } else { - component_block.body.unshift(store_init); - component_block.body.push(.../** @type {ESTree.Statement[]} */ (template.body)); - } - // trick esrap into including comments component_block.loc = instance.loc; @@ -621,8 +604,9 @@ export function client_component(analysis, options) { ); } - if (analysis.custom_element) { - const ce = analysis.custom_element; + const ce = options.customElementOptions ?? options.customElement; + + if (ce) { const ce_props = typeof ce === 'boolean' ? {} : ce.props || {}; /** @type {ESTree.Property[]} */ 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..8ae67f49d1 --- /dev/null +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ForOfStatement.js @@ -0,0 +1,25 @@ +/** @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') && + context.state.options.experimental.async + ) { + const left = /** @type {VariableDeclaration | Pattern} */ (context.visit(node.left)); + const argument = /** @type {Expression} */ (context.visit(node.right)); + const body = /** @type {Statement} */ (context.visit(node.body)); + 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..85d8e3caff 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Fragment.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Fragment.js @@ -48,6 +48,7 @@ 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 @@ -61,6 +62,7 @@ export function Fragment(node, context) { const state = { ...context.state, init: [], + consts: [], update: [], after_update: [], memoizer: new Memoizer(), @@ -76,11 +78,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 +93,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 +117,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 +137,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 +150,17 @@ export function Fragment(node, context) { } } + 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 +176,9 @@ export function Fragment(node, context) { body.push(close); } - return b.block(body); + if (has_await) { + return b.block([b.stmt(b.call('$.async_body', b.arrow([], b.block(body), true)))]); + } else { + 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..49c89bc438 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteBoundary.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteBoundary.js @@ -39,40 +39,60 @@ export function SvelteBoundary(node, context) { /** @type {Statement[]} */ const hoisted = []; + let has_const = false; + // const tags need to live inside the boundary, but might also be referenced in hoisted snippets. // to resolve this we cheat: we duplicate const tags inside snippets + // We'll revert this behavior in the future, it was a mistake to allow this (Component snippets also don't do this). for (const child of node.fragment.nodes) { if (child.type === 'ConstTag') { - context.visit(child, { ...context.state, init: const_tags }); + has_const = true; + if (!context.state.options.experimental.async) { + context.visit(child, { ...context.state, consts: const_tags }); + } } } for (const child of node.fragment.nodes) { if (child.type === 'ConstTag') { + if (context.state.options.experimental.async) { + nodes.push(child); + } continue; } if (child.type === 'SnippetBlock') { - /** @type {Statement[]} */ - const statements = []; - - context.visit(child, { ...context.state, init: statements }); - - const snippet = /** @type {VariableDeclaration} */ (statements[0]); - - const snippet_fn = dev - ? // @ts-expect-error we know this shape is correct - snippet.declarations[0].init.arguments[1] - : snippet.declarations[0].init; - - snippet_fn.body.body.unshift( - ...const_tags.filter((node) => node.type === 'VariableDeclaration') - ); - - hoisted.push(snippet); - - if (['failed', 'pending'].includes(child.expression.name)) { - props.properties.push(b.prop('init', child.expression, child.expression)); + if ( + context.state.options.experimental.async && + has_const && + !['failed', 'pending'].includes(child.expression.name) + ) { + // we can't hoist snippets as they may reference const tags, so we just keep them in the fragment + nodes.push(child); + } else { + /** @type {Statement[]} */ + const statements = []; + + context.visit(child, { ...context.state, init: statements }); + + const snippet = /** @type {VariableDeclaration} */ (statements[0]); + + const snippet_fn = dev + ? // @ts-expect-error we know this shape is correct + snippet.declarations[0].init.arguments[1] + : snippet.declarations[0].init; + + if (!context.state.options.experimental.async) { + snippet_fn.body.body.unshift( + ...const_tags.filter((node) => node.type === 'VariableDeclaration') + ); + } + + if (['failed', 'pending'].includes(child.expression.name)) { + props.properties.push(b.prop('init', child.expression, child.expression)); + } + + hoisted.push(snippet); } continue; @@ -83,7 +103,9 @@ export function SvelteBoundary(node, context) { const block = /** @type {BlockStatement} */ (context.visit({ ...node.fragment, nodes })); - block.body.unshift(...const_tags); + if (!context.state.options.experimental.async) { + block.body.unshift(...const_tags); + } const boundary = b.stmt( b.call('$.boundary', context.state.node, props, b.arrow([b.id('$$anchor')], block)) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js index 014547cf2d..ba140a153e 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js @@ -34,7 +34,7 @@ export class Memoizer { } apply() { - return [...this.#async, ...this.#sync].map((memo, i) => { + return [...this.#sync, ...this.#async].map((memo, i) => { memo.id.name = `$${i}`; return memo.id; }); diff --git a/packages/svelte/src/compiler/phases/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/nodes.js b/packages/svelte/src/compiler/phases/nodes.js index 4874554ff0..f4127db359 100644 --- a/packages/svelte/src/compiler/phases/nodes.js +++ b/packages/svelte/src/compiler/phases/nodes.js @@ -23,6 +23,15 @@ export function is_element_node(node) { return element_nodes.includes(node.type); } +/** + * Returns true for all component-like nodes + * @param {AST.SvelteNode} node + * @returns {node is AST.Component | AST.SvelteComponent | AST.SvelteSelf} + */ +export function is_component_node(node) { + return ['Component', 'SvelteComponent', 'SvelteSelf'].includes(node.type); +} + /** * @param {AST.RegularElement | AST.SvelteElement} node * @returns {boolean} diff --git a/packages/svelte/src/compiler/phases/scope.js b/packages/svelte/src/compiler/phases/scope.js index 700e098e45..76157d406f 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], @@ -122,7 +122,7 @@ export class Binding { /** * Additional metadata, varies per binding type - * @type {null | { inside_rest?: boolean }} + * @type {null | { inside_rest?: boolean; is_template_declaration?: boolean }} */ metadata = null; @@ -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') { @@ -1110,6 +1121,7 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) { node.kind, declarator.init ); + binding.metadata = { is_template_declaration: true }; bindings.push(binding); } } diff --git a/packages/svelte/src/compiler/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/compiler/validate-options.js b/packages/svelte/src/compiler/validate-options.js index ed83375d22..2b727ad093 100644 --- a/packages/svelte/src/compiler/validate-options.js +++ b/packages/svelte/src/compiler/validate-options.js @@ -8,7 +8,7 @@ import * as w from './warnings.js'; * @typedef {(input: Input, keypath: string) => Required} Validator */ -const common = { +const common_options = { filename: string('(unknown)'), // default to process.cwd() where it exists to replicate svelte4 behavior (and make Deno work with this as well) @@ -48,110 +48,120 @@ const common = { }) }; -export const validate_module_options = - /** @type {Validator} */ ( - object({ - ...common - }) - ); +const component_options = { + accessors: deprecate(w.options_deprecated_accessors, boolean(false)), -export const validate_component_options = - /** @type {Validator} */ ( - object({ - ...common, + css: validator('external', (input) => { + if (input === true || input === false) { + throw_error( + 'The boolean options have been removed from the css option. Use "external" instead of false and "injected" instead of true' + ); + } + if (input === 'none') { + throw_error( + 'css: "none" is no longer a valid option. If this was crucial for you, please open an issue on GitHub with your use case.' + ); + } - accessors: deprecate(w.options_deprecated_accessors, boolean(false)), + if (input !== 'external' && input !== 'injected') { + throw_error(`css should be either "external" (default, recommended) or "injected"`); + } - css: validator('external', (input) => { - if (input === true || input === false) { - throw_error( - 'The boolean options have been removed from the css option. Use "external" instead of false and "injected" instead of true' - ); - } - if (input === 'none') { - throw_error( - 'css: "none" is no longer a valid option. If this was crucial for you, please open an issue on GitHub with your use case.' - ); - } + return input; + }), - if (input !== 'external' && input !== 'injected') { - throw_error(`css should be either "external" (default, recommended) or "injected"`); - } + cssHash: fun(({ css, hash }) => { + return `svelte-${hash(css)}`; + }), + + // TODO this is a sourcemap option, would be good to put under a sourcemap namespace + cssOutputFilename: string(undefined), + + customElement: boolean(false), + + discloseVersion: boolean(true), - return input; - }), + immutable: deprecate(w.options_deprecated_immutable, boolean(false)), - cssHash: fun(({ css, hash }) => { - return `svelte-${hash(css)}`; - }), + legacy: removed( + 'The legacy option has been removed. If you are using this because of legacy.componentApi, use compatibility.componentApi instead' + ), + + compatibility: object({ + componentApi: list([4, 5], 5) + }), + + loopGuardTimeout: warn_removed(w.options_removed_loop_guard_timeout), + + name: string(undefined), - // TODO this is a sourcemap option, would be good to put under a sourcemap namespace - cssOutputFilename: string(undefined), + namespace: list(['html', 'mathml', 'svg']), - customElement: boolean(false), + modernAst: boolean(false), - discloseVersion: boolean(true), + outputFilename: string(undefined), - immutable: deprecate(w.options_deprecated_immutable, boolean(false)), + preserveComments: boolean(false), - legacy: removed( - 'The legacy option has been removed. If you are using this because of legacy.componentApi, use compatibility.componentApi instead' - ), + fragments: list(['html', 'tree']), - compatibility: object({ - componentApi: list([4, 5], 5) - }), + preserveWhitespace: boolean(false), - loopGuardTimeout: warn_removed(w.options_removed_loop_guard_timeout), + runes: boolean(undefined), - name: string(undefined), + hmr: boolean(false), - namespace: list(['html', 'mathml', 'svg']), + sourcemap: validator(undefined, (input) => { + // Source maps can take on a variety of values, including string, JSON, map objects from magic-string and source-map, + // so there's no good way to check type validity here + return input; + }), - modernAst: boolean(false), + enableSourcemap: warn_removed(w.options_removed_enable_sourcemap), - outputFilename: string(undefined), + hydratable: warn_removed(w.options_removed_hydratable), - preserveComments: boolean(false), + format: removed( + 'The format option has been removed in Svelte 4, the compiler only outputs ESM now. Remove "format" from your compiler options. ' + + 'If you did not set this yourself, bump the version of your bundler plugin (vite-plugin-svelte/rollup-plugin-svelte/svelte-loader)' + ), - fragments: list(['html', 'tree']), + tag: removed( + 'The tag option has been removed in Svelte 5. Use `` inside the component instead. ' + + 'If that does not solve your use case, please open an issue on GitHub with details.' + ), - preserveWhitespace: boolean(false), + sveltePath: removed( + 'The sveltePath option has been removed in Svelte 5. ' + + 'If this option was crucial for you, please open an issue on GitHub with your use case.' + ), - runes: boolean(undefined), + // These two were primarily created for svelte-preprocess (https://github.com/sveltejs/svelte/pull/6194), + // but with new TypeScript compilation modes strictly separating types it's not necessary anymore + errorMode: removed( + 'The errorMode option has been removed. If you are using this through svelte-preprocess with TypeScript, ' + + 'use the https://www.typescriptlang.org/tsconfig#verbatimModuleSyntax setting instead' + ), - hmr: boolean(false), + varsReport: removed( + 'The vars option has been removed. If you are using this through svelte-preprocess with TypeScript, ' + + 'use the https://www.typescriptlang.org/tsconfig#verbatimModuleSyntax setting instead' + ) +}; - sourcemap: validator(undefined, (input) => { - // Source maps can take on a variety of values, including string, JSON, map objects from magic-string and source-map, - // so there's no good way to check type validity here - return input; - }), +export const validate_module_options = + /** @type {Validator} */ ( + object({ + ...common_options, + ...Object.fromEntries(Object.keys(component_options).map((key) => [key, () => {}])) + }) + ); - enableSourcemap: warn_removed(w.options_removed_enable_sourcemap), - hydratable: warn_removed(w.options_removed_hydratable), - format: removed( - 'The format option has been removed in Svelte 4, the compiler only outputs ESM now. Remove "format" from your compiler options. ' + - 'If you did not set this yourself, bump the version of your bundler plugin (vite-plugin-svelte/rollup-plugin-svelte/svelte-loader)' - ), - tag: removed( - 'The tag option has been removed in Svelte 5. Use `` inside the component instead. ' + - 'If that does not solve your use case, please open an issue on GitHub with details.' - ), - sveltePath: removed( - 'The sveltePath option has been removed in Svelte 5. ' + - 'If this option was crucial for you, please open an issue on GitHub with your use case.' - ), - // These two were primarily created for svelte-preprocess (https://github.com/sveltejs/svelte/pull/6194), - // but with new TypeScript compilation modes strictly separating types it's not necessary anymore - errorMode: removed( - 'The errorMode option has been removed. If you are using this through svelte-preprocess with TypeScript, ' + - 'use the https://www.typescriptlang.org/tsconfig#verbatimModuleSyntax setting instead' - ), - varsReport: removed( - 'The vars option has been removed. If you are using this through svelte-preprocess with TypeScript, ' + - 'use the https://www.typescriptlang.org/tsconfig#verbatimModuleSyntax setting instead' - ) +export const validate_component_options = + /** @type {Validator} */ ( + object({ + ...common_options, + ...component_options }) ); 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/dev/inspect.js b/packages/svelte/src/internal/client/dev/inspect.js index c593f2622c..f79cf47299 100644 --- a/packages/svelte/src/internal/client/dev/inspect.js +++ b/packages/svelte/src/internal/client/dev/inspect.js @@ -26,7 +26,7 @@ export function inspect(get_value, inspector = console.log) { return; } - var snap = snapshot(value, true); + var snap = snapshot(value, true, true); untrack(() => { inspector(initial ? 'init' : 'update', ...snap); }); 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/css.js b/packages/svelte/src/internal/client/dom/css.js index 52be36aa1f..8e6faa0e32 100644 --- a/packages/svelte/src/internal/client/dom/css.js +++ b/packages/svelte/src/internal/client/dom/css.js @@ -1,6 +1,6 @@ import { DEV } from 'esm-env'; -import { queue_micro_task } from './task.js'; import { register_style } from '../dev/css.js'; +import { effect } from '../reactivity/effects.js'; /** * @param {Node} anchor @@ -8,7 +8,7 @@ import { register_style } from '../dev/css.js'; */ export function append_styles(anchor, css) { // Use `queue_micro_task` to ensure `anchor` is in the DOM, otherwise getRootNode() will yield wrong results - queue_micro_task(() => { + effect(() => { var root = anchor.getRootNode(); var target = /** @type {ShadowRoot} */ (root).host diff --git a/packages/svelte/src/internal/client/dom/elements/attributes.js b/packages/svelte/src/internal/client/dom/elements/attributes.js index 0081dad1cf..f95ba3c6fb 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'; @@ -466,6 +466,8 @@ export function set_attributes( ) { // @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 7c1fccea0f..67e6ff1dd2 100644 --- a/packages/svelte/src/internal/client/dom/elements/bindings/input.js +++ b/packages/svelte/src/internal/client/dom/elements/bindings/input.js @@ -8,7 +8,7 @@ import { queue_micro_task } from '../../task.js'; import { hydrating } from '../../hydration.js'; import { untrack } from '../../../runtime.js'; import { is_runes } from '../../../context.js'; -import { current_batch } from '../../../reactivity/batch.js'; +import { current_batch, previous_batch } from '../../../reactivity/batch.js'; /** * @param {HTMLInputElement} input @@ -76,13 +76,18 @@ export function bind_value(input, get, set = get) { var value = get(); - if (input === document.activeElement && batches.has(/** @type {Batch} */ (current_batch))) { + if (input === document.activeElement) { + // we need both, because in non-async mode, render effects run before previous_batch is set + var batch = /** @type {Batch} */ (previous_batch ?? current_batch); + // Never rewrite the contents of a focused input. We can get here if, for example, // an update is deferred because of async work depending on the input: // // //

{await find(query)}

- return; + if (batches.has(batch)) { + return; + } } if (is_numberlike_input(input) && value === to_number(input.value)) { @@ -240,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/operations.js b/packages/svelte/src/internal/client/dom/operations.js index fb269e47e0..abc29a7670 100644 --- a/packages/svelte/src/internal/client/dom/operations.js +++ b/packages/svelte/src/internal/client/dom/operations.js @@ -6,6 +6,7 @@ import { get_descriptor, is_extensible } from '../../shared/utils.js'; import { active_effect } from '../runtime.js'; import { async_mode_flag } from '../../flags/index.js'; import { TEXT_NODE, EFFECT_RAN } from '#client/constants'; +import { eager_block_effects } from '../reactivity/batch.js'; // export these for reference in the compiled code, making global name deduplication unnecessary /** @type {Window} */ @@ -214,6 +215,7 @@ export function clear_text_content(node) { */ export function should_defer_append() { if (!async_mode_flag) return false; + if (eager_block_effects !== null) return false; var flags = /** @type {Effect} */ (active_effect).f; return (flags & EFFECT_RAN) !== 0; 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..c5b7bb845c 100644 --- a/packages/svelte/src/internal/client/index.js +++ b/packages/svelte/src/internal/client/index.js @@ -98,7 +98,12 @@ export { props_id, with_script } from './dom/template.js'; -export { save, track_reactivity_loss } from './reactivity/async.js'; +export { + async_body, + 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, @@ -147,7 +152,8 @@ export { untrack, exclude_from_object, deep_read, - deep_read_state + deep_read_state, + active_effect } from './runtime.js'; export { validate_binding, validate_each_keys } from './validate.js'; export { raf } from './timing.js'; @@ -172,3 +178,4 @@ export { } from '../shared/validate.js'; export { strict_equals, equals } from './dev/equality.js'; export { log_if_contains_state } from './dev/console-log.js'; +export { invoke_error_boundary } from './error-handling.js'; diff --git a/packages/svelte/src/internal/client/proxy.js b/packages/svelte/src/internal/client/proxy.js index 3ae4b87ed5..dae3791eb0 100644 --- a/packages/svelte/src/internal/client/proxy.js +++ b/packages/svelte/src/internal/client/proxy.js @@ -93,9 +93,11 @@ export function proxy(value) { /** Used in dev for $inspect.trace() */ var path = ''; - + let updating = false; /** @param {string} new_path */ function update_path(new_path) { + if (updating) return; + updating = true; path = new_path; tag(version, `${path} version`); @@ -104,6 +106,7 @@ export function proxy(value) { for (const [prop, source] of sources) { tag(source, get_label(path, prop)); } + updating = false; } return new Proxy(/** @type {any} */ (value), { @@ -284,13 +287,13 @@ export function proxy(value) { if (s === undefined) { if (!has || get_descriptor(target, prop)?.writable) { s = with_parent(() => source(undefined, stack)); - set(s, proxy(value)); - - sources.set(prop, s); if (DEV) { tag(s, get_label(path, prop)); } + set(s, proxy(value)); + + sources.set(prop, s); } } else { has = s.v !== UNINITIALIZED; diff --git a/packages/svelte/src/internal/client/reactivity/async.js b/packages/svelte/src/internal/client/reactivity/async.js index c200f10dba..1ea1bbe561 100644 --- a/packages/svelte/src/internal/client/reactivity/async.js +++ b/packages/svelte/src/internal/client/reactivity/async.js @@ -11,7 +11,7 @@ import { set_active_effect, set_active_reaction } from '../runtime.js'; -import { current_batch } from './batch.js'; +import { current_batch, suspend } from './batch.js'; import { async_derived, current_async_effect, @@ -19,6 +19,7 @@ import { derived_safe_equal, set_from_async_derived } from './deriveds.js'; +import { aborted } from './effects.js'; /** * @@ -119,9 +120,72 @@ 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); set_component_context(null); if (DEV) set_from_async_derived(null); } + +/** + * @param {() => Promise} fn + */ +export async function async_body(fn) { + const unsuspend = suspend(); + const active = /** @type {Effect} */ (active_effect); + + try { + await fn(); + } catch (error) { + if (!aborted(active)) { + invoke_error_boundary(error, active); + } + } finally { + unsuspend(); + } +} diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index c452211894..60fa03c56c 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -10,7 +10,8 @@ import { INERT, RENDER_EFFECT, ROOT_EFFECT, - USER_EFFECT + USER_EFFECT, + MAYBE_DIRTY } from '#client/constants'; import { async_mode_flag } from '../../flags/index.js'; import { deferred, define_property } from '../../shared/utils.js'; @@ -21,8 +22,7 @@ import { is_updating_effect, set_is_updating_effect, set_signal_status, - update_effect, - write_version + update_effect } from '../runtime.js'; import * as e from '../errors.js'; import { flush_tasks } from '../dom/task.js'; @@ -38,6 +38,13 @@ const batches = new Set(); /** @type {Batch | null} */ export let current_batch = null; +/** + * This is needed to avoid overwriting inputs in non-async mode + * TODO 6.0 remove this, as non-async mode will go away + * @type {Batch | null} + */ +export let previous_batch = null; + /** * When time travelling, we re-evaluate deriveds based on the temporary * values of their dependencies rather than their actual values, and cache @@ -70,13 +77,14 @@ let last_scheduled_effect = null; let is_flushing = false; +let is_flushing_sync = false; export class Batch { /** * The current values of any sources that are updated in this batch * They keys of this map are identical to `this.#previous` * @type {Map} */ - #current = new Map(); + current = new Map(); /** * The values of any sources that are updated in this batch _before_ those updates took place. @@ -145,6 +153,18 @@ export class Batch { */ #block_effects = []; + /** + * Deferred effects (which run after async work has completed) that are DIRTY + * @type {Effect[]} + */ + #dirty_effects = []; + + /** + * Deferred effects that are MAYBE_DIRTY + * @type {Effect[]} + */ + #maybe_dirty_effects = []; + /** * A set of branches that still exist, but will be destroyed when this batch * is committed — we skip over these during `process` @@ -156,9 +176,11 @@ export class Batch { * * @param {Effect[]} root_effects */ - #process(root_effects) { + process(root_effects) { queued_root_effects = []; + previous_batch = null; + /** @type {Map | null} */ var current_values = null; @@ -169,7 +191,7 @@ export class Batch { current_values = new Map(); batch_deriveds = new Map(); - for (const [source, current] of this.#current) { + for (const [source, current] of this.current) { current_values.set(source, { v: source.v, wv: source.wv }); source.v = current; } @@ -202,15 +224,28 @@ export class Batch { this.#effects = []; this.#block_effects = []; + // If sources are written to, then work needs to happen in a separate batch, else prior sources would be mixed with + // newly updated sources, which could lead to infinite loops when effects run over and over again. + previous_batch = current_batch; + current_batch = null; + flush_queued_effects(render_effects); flush_queued_effects(effects); + // Reinstate the current batch if there was no new one created, as `process()` runs in a loop in `flush_effects()`. + // That method expects `current_batch` to be set, and could run the loop again if effects result in new effects + // being scheduled but without writes happening in which case no new batch is created. + if (current_batch === null) { + current_batch = this; + } else { + batches.delete(this); + } + this.#deferred?.resolve(); } else { - // otherwise mark effects clean so they get scheduled on the next run - for (const e of this.#render_effects) set_signal_status(e, CLEAN); - for (const e of this.#effects) set_signal_status(e, CLEAN); - for (const e of this.#block_effects) set_signal_status(e, CLEAN); + this.#defer_effects(this.#render_effects); + this.#defer_effects(this.#effects); + this.#defer_effects(this.#block_effects); } if (current_values) { @@ -261,11 +296,11 @@ export class Batch { this.#effects.push(effect); } else if (async_mode_flag && (flags & RENDER_EFFECT) !== 0) { this.#render_effects.push(effect); - } else if (is_dirty(effect)) { + } else if ((flags & CLEAN) === 0) { if ((flags & ASYNC) !== 0) { var effects = effect.b?.pending ? this.#boundary_async_effects : this.#async_effects; effects.push(effect); - } else { + } else if (is_dirty(effect)) { if ((effect.f & BLOCK_EFFECT) !== 0) this.#block_effects.push(effect); update_effect(effect); } @@ -289,6 +324,21 @@ export class Batch { } } + /** + * @param {Effect[]} effects + */ + #defer_effects(effects) { + for (const e of effects) { + const target = (e.f & DIRTY) !== 0 ? this.#dirty_effects : this.#maybe_dirty_effects; + target.push(e); + + // mark as clean so they get scheduled if they depend on pending async state + set_signal_status(e, CLEAN); + } + + effects.length = 0; + } + /** * Associate a change to a given source with the current * batch, noting its previous and current values @@ -300,7 +350,7 @@ export class Batch { this.#previous.set(source, value); } - this.#current.set(source, source.v); + this.current.set(source, source.v); } activate() { @@ -309,6 +359,7 @@ export class Batch { deactivate() { current_batch = null; + previous_batch = null; for (const update of effect_pending_updates) { effect_pending_updates.delete(update); @@ -327,13 +378,13 @@ export class Batch { flush() { if (queued_root_effects.length > 0) { - this.flush_effects(); + flush_effects(); } else { this.#commit(); } if (current_batch !== this) { - // this can happen if a `flushSync` occurred during `this.flush_effects()`, + // this can happen if a `flushSync` occurred during `flush_effects()`, // which is permitted in legacy mode despite being a terrible idea return; } @@ -345,52 +396,6 @@ export class Batch { this.deactivate(); } - flush_effects() { - var was_updating_effect = is_updating_effect; - is_flushing = true; - - try { - var flush_count = 0; - set_is_updating_effect(true); - - while (queued_root_effects.length > 0) { - if (flush_count++ > 1000) { - if (DEV) { - var updates = new Map(); - - for (const source of this.#current.keys()) { - for (const [stack, update] of source.updated ?? []) { - var entry = updates.get(stack); - - if (!entry) { - entry = { error: update.error, count: 0 }; - updates.set(stack, entry); - } - - entry.count += update.count; - } - } - - for (const update of updates.values()) { - // eslint-disable-next-line no-console - console.error(update.error); - } - } - - infinite_loop_guard(); - } - - this.#process(queued_root_effects); - old_values.clear(); - } - } finally { - is_flushing = false; - set_is_updating_effect(was_updating_effect); - - last_scheduled_effect = null; - } - } - /** * Append and remove branches to/from the DOM */ @@ -412,18 +417,13 @@ export class Batch { this.#pending -= 1; if (this.#pending === 0) { - for (const e of this.#render_effects) { - set_signal_status(e, DIRTY); - schedule_effect(e); - } - - for (const e of this.#effects) { + for (const e of this.#dirty_effects) { set_signal_status(e, DIRTY); schedule_effect(e); } - for (const e of this.#block_effects) { - set_signal_status(e, DIRTY); + for (const e of this.#maybe_dirty_effects) { + set_signal_status(e, MAYBE_DIRTY); schedule_effect(e); } @@ -445,12 +445,12 @@ export class Batch { return (this.#deferred ??= deferred()).promise; } - static ensure(autoflush = true) { + static ensure() { if (current_batch === null) { const batch = (current_batch = new Batch()); batches.add(current_batch); - if (autoflush) { + if (!is_flushing_sync) { Batch.enqueue(() => { if (current_batch !== batch) { // a flushSync happened in the meantime @@ -487,32 +487,85 @@ export function flushSync(fn) { e.flush_sync_in_effect(); } - var result; + var was_flushing_sync = is_flushing_sync; + is_flushing_sync = true; - const batch = Batch.ensure(false); + try { + var result; - if (fn) { - batch.flush_effects(); + if (fn) { + flush_effects(); + result = fn(); + } - result = fn(); - } + while (true) { + flush_tasks(); - while (true) { - flush_tasks(); + if (queued_root_effects.length === 0) { + current_batch?.flush(); - if (queued_root_effects.length === 0) { - if (batch === current_batch) { - batch.flush(); + // we need to check again, in case we just updated an `$effect.pending()` + if (queued_root_effects.length === 0) { + // this would be reset in `flush_effects()` but since we are early returning here, + // we need to reset it here as well in case the first time there's 0 queued root effects + last_scheduled_effect = null; + + return /** @type {T} */ (result); + } } - // this would be reset in `batch.flush_effects()` but since we are early returning here, - // we need to reset it here as well in case the first time there's 0 queued root effects - last_scheduled_effect = null; + flush_effects(); + } + } finally { + is_flushing_sync = was_flushing_sync; + } +} + +function flush_effects() { + var was_updating_effect = is_updating_effect; + is_flushing = true; + + try { + var flush_count = 0; + set_is_updating_effect(true); + + while (queued_root_effects.length > 0) { + var batch = Batch.ensure(); + + if (flush_count++ > 1000) { + if (DEV) { + var updates = new Map(); - return /** @type {T} */ (result); + for (const source of batch.current.keys()) { + for (const [stack, update] of source.updated ?? []) { + var entry = updates.get(stack); + + if (!entry) { + entry = { error: update.error, count: 0 }; + updates.set(stack, entry); + } + + entry.count += update.count; + } + } + + for (const update of updates.values()) { + // eslint-disable-next-line no-console + console.error(update.error); + } + } + + infinite_loop_guard(); + } + + batch.process(queued_root_effects); + old_values.clear(); } + } finally { + is_flushing = false; + set_is_updating_effect(was_updating_effect); - batch.flush_effects(); + last_scheduled_effect = null; } } @@ -531,6 +584,9 @@ function infinite_loop_guard() { } } +/** @type {Effect[] | null} */ +export let eager_block_effects = null; + /** * @param {Array} effects * @returns {void} @@ -545,7 +601,7 @@ function flush_queued_effects(effects) { var effect = effects[i++]; if ((effect.f & (DESTROYED | INERT)) === 0 && is_dirty(effect)) { - var wv = write_version; + eager_block_effects = []; update_effect(effect); @@ -566,17 +622,20 @@ function flush_queued_effects(effects) { } } - // if state is written in a user effect, abort and re-schedule, lest we run - // effects that should be removed as a result of the state change - if (write_version > wv && (effect.f & USER_EFFECT) !== 0) { - break; + if (eager_block_effects.length > 0) { + // TODO this feels incorrect! it gets the tests passing + old_values.clear(); + + for (const e of eager_block_effects) { + update_effect(e); + } + + eager_block_effects = []; } } } - while (i < length) { - schedule_effect(effects[i++]); - } + eager_block_effects = null; } /** diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index c4edd2bf8d..2c9e4db911 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 @@ -132,25 +133,40 @@ function create_effect(type, fn, sync, push = true) { schedule_effect(effect); } - // if an effect has no dependencies, no DOM and no teardown function, - // don't bother adding it to the effect tree - var inert = - sync && - effect.deps === null && - effect.first === null && - effect.nodes_start === null && - effect.teardown === null && - (effect.f & EFFECT_PRESERVED) === 0; - - if (!inert && push) { - if (parent !== null) { - push_effect(effect, parent); + if (push) { + /** @type {Effect | null} */ + var e = effect; + + // if an effect has already ran and doesn't need to be kept in the tree + // (because it won't re-run, has no DOM, and has no teardown etc) + // then we skip it and go to its child (if any) + if ( + sync && + e.deps === null && + e.teardown === null && + e.nodes_start === null && + e.first === e.last && // either `null`, or a singular child + (e.f & EFFECT_PRESERVED) === 0 + ) { + e = e.first; } - // if we're in a derived, add the effect there too - if (active_reaction !== null && (active_reaction.f & DERIVED) !== 0) { - var derived = /** @type {Derived} */ (active_reaction); - (derived.effects ??= []).push(effect); + if (e !== null) { + e.parent = parent; + + if (parent !== null) { + push_effect(e, parent); + } + + // if we're in a derived, add the effect there too + if ( + active_reaction !== null && + (active_reaction.f & DERIVED) !== 0 && + (type & ROOT_EFFECT) === 0 + ) { + var derived = /** @type {Derived} */ (active_reaction); + (derived.effects ??= []).push(e); + } } } @@ -237,7 +253,7 @@ export function inspect_effect(fn) { */ export function effect_root(fn) { Batch.ensure(); - const effect = create_effect(ROOT_EFFECT, fn, true); + const effect = create_effect(ROOT_EFFECT | EFFECT_PRESERVED, fn, true); return () => { destroy_effect(effect); @@ -251,7 +267,7 @@ export function effect_root(fn) { */ export function component_root(fn) { Batch.ensure(); - const effect = create_effect(ROOT_EFFECT, fn, true); + const effect = create_effect(ROOT_EFFECT | EFFECT_PRESERVED, fn, true); return (options = {}) => { return new Promise((fulfil) => { @@ -370,7 +386,7 @@ export function block(fn, flags = 0) { * @param {boolean} [push] */ export function branch(fn, push = true) { - return create_effect(BRANCH_EFFECT, fn, true, push); + return create_effect(BRANCH_EFFECT | EFFECT_PRESERVED, fn, true, push); } /** @@ -402,7 +418,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; @@ -637,7 +659,6 @@ function resume_children(effect, local) { } } -export function aborted() { - var effect = /** @type {Effect} */ (active_effect); +export function aborted(effect = /** @type {Effect} */ (active_effect)) { return (effect.f & DESTROYED) !== 0; } 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 f6b14f3360..cd0c28016d 100644 --- a/packages/svelte/src/internal/client/reactivity/sources.js +++ b/packages/svelte/src/internal/client/reactivity/sources.js @@ -33,10 +33,11 @@ import * as e from '../errors.js'; import { legacy_mode_flag, tracing_mode_flag } from '../../flags/index.js'; import { get_stack, tag_proxy } from '../dev/tracing.js'; import { component_context, is_runes } from '../context.js'; -import { Batch, schedule_effect } from './batch.js'; +import { Batch, eager_block_effects, 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} */ @@ -179,7 +180,7 @@ export function internal_set(source, value) { source.v = value; - const batch = Batch.ensure(); + var batch = Batch.ensure(); batch.capture(source, old_value); if (DEV) { @@ -323,14 +324,22 @@ function mark_reactions(signal, status) { continue; } + var not_dirty = (flags & DIRTY) === 0; + // don't set a DIRTY reaction to MAYBE_DIRTY - if ((flags & DIRTY) === 0) { + if (not_dirty) { set_signal_status(reaction, status); } if ((flags & DERIVED) !== 0) { mark_reactions(/** @type {Derived} */ (reaction), MAYBE_DIRTY); - } else if ((flags & DIRTY) === 0) { + } else if (not_dirty) { + if ((flags & BLOCK_EFFECT) !== 0) { + if (eager_block_effects !== null) { + eager_block_effects.push(/** @type {Effect} */ (reaction)); + } + } + schedule_effect(/** @type {Effect} */ (reaction)); } } diff --git a/packages/svelte/src/internal/client/reactivity/types.d.ts b/packages/svelte/src/internal/client/reactivity/types.d.ts index 72187e84a7..81f7197b80 100644 --- a/packages/svelte/src/internal/client/reactivity/types.d.ts +++ b/packages/svelte/src/internal/client/reactivity/types.d.ts @@ -54,7 +54,7 @@ export interface Reaction extends Signal { export interface Derived extends Value, Reaction { /** The derived function */ fn: () => V; - /** Effects created inside this signal */ + /** Effects created inside this signal. Used to destroy those effects when the derived reruns or is cleaned up */ effects: null | Effect[]; /** Parent effect or derived */ parent: Effect | Derived | null; 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/clone.js b/packages/svelte/src/internal/shared/clone.js index 4632fc3d68..b8f99ee198 100644 --- a/packages/svelte/src/internal/shared/clone.js +++ b/packages/svelte/src/internal/shared/clone.js @@ -15,14 +15,15 @@ const empty = []; * @template T * @param {T} value * @param {boolean} [skip_warning] + * @param {boolean} [no_tojson] * @returns {Snapshot} */ -export function snapshot(value, skip_warning = false) { +export function snapshot(value, skip_warning = false, no_tojson = false) { if (DEV && !skip_warning) { /** @type {string[]} */ const paths = []; - const copy = clone(value, new Map(), '', paths); + const copy = clone(value, new Map(), '', paths, null, no_tojson); if (paths.length === 1 && paths[0] === '') { // value could not be cloned w.state_snapshot_uncloneable(); @@ -40,7 +41,7 @@ export function snapshot(value, skip_warning = false) { return copy; } - return clone(value, new Map(), '', empty); + return clone(value, new Map(), '', empty, null, no_tojson); } /** @@ -49,10 +50,11 @@ export function snapshot(value, skip_warning = false) { * @param {Map>} cloned * @param {string} path * @param {string[]} paths - * @param {null | T} original The original value, if `value` was produced from a `toJSON` call + * @param {null | T} [original] The original value, if `value` was produced from a `toJSON` call + * @param {boolean} [no_tojson] * @returns {Snapshot} */ -function clone(value, cloned, path, paths, original = null) { +function clone(value, cloned, path, paths, original = null, no_tojson = false) { if (typeof value === 'object' && value !== null) { var unwrapped = cloned.get(value); if (unwrapped !== undefined) return unwrapped; @@ -71,7 +73,7 @@ function clone(value, cloned, path, paths, original = null) { for (var i = 0; i < value.length; i += 1) { var element = value[i]; if (i in value) { - copy[i] = clone(element, cloned, DEV ? `${path}[${i}]` : path, paths); + copy[i] = clone(element, cloned, DEV ? `${path}[${i}]` : path, paths, null, no_tojson); } } @@ -88,8 +90,15 @@ function clone(value, cloned, path, paths, original = null) { } for (var key in value) { - // @ts-expect-error - copy[key] = clone(value[key], cloned, DEV ? `${path}.${key}` : path, paths); + copy[key] = clone( + // @ts-expect-error + value[key], + cloned, + DEV ? `${path}.${key}` : path, + paths, + null, + no_tojson + ); } return copy; @@ -99,7 +108,7 @@ function clone(value, cloned, path, paths, original = null) { return /** @type {Snapshot} */ (structuredClone(value)); } - if (typeof (/** @type {T & { toJSON?: any } } */ (value).toJSON) === 'function') { + if (typeof (/** @type {T & { toJSON?: any } } */ (value).toJSON) === 'function' && !no_tojson) { return clone( /** @type {T & { toJSON(): any } } */ (value).toJSON(), cloned, 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 7d47fbc5f1..815b25bf1f 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.36.13'; +export const VERSION = '5.38.3'; 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/const-tag-snippet-invalid-reference-1/_config.js b/packages/svelte/tests/compiler-errors/samples/const-tag-snippet-invalid-reference-1/_config.js new file mode 100644 index 0000000000..7424278180 --- /dev/null +++ b/packages/svelte/tests/compiler-errors/samples/const-tag-snippet-invalid-reference-1/_config.js @@ -0,0 +1,10 @@ +import { test } from '../../test'; + +export default test({ + async: true, + error: { + code: 'const_tag_invalid_reference', + message: 'The `{@const foo = ...}` declaration is not available in this snippet ', + position: [376, 379] + } +}); diff --git a/packages/svelte/tests/compiler-errors/samples/const-tag-snippet-invalid-reference-1/main.svelte b/packages/svelte/tests/compiler-errors/samples/const-tag-snippet-invalid-reference-1/main.svelte new file mode 100644 index 0000000000..a2533e33b0 --- /dev/null +++ b/packages/svelte/tests/compiler-errors/samples/const-tag-snippet-invalid-reference-1/main.svelte @@ -0,0 +1,32 @@ + + + + + {@const foo = 'bar'} + + {#snippet other()} + {foo} + {/snippet} + + {foo} + + + {#snippet failed()} + {foo} + {/snippet} + + + {#snippet failed()} + {@const foo = 'bar'} + {foo} + {/snippet} + + + + + {@const foo = 'bar'} + + {#snippet failed()} + {foo} + {/snippet} + diff --git a/packages/svelte/tests/compiler-errors/samples/const-tag-snippet-invalid-reference-2/_config.js b/packages/svelte/tests/compiler-errors/samples/const-tag-snippet-invalid-reference-2/_config.js new file mode 100644 index 0000000000..7ff71a61f9 --- /dev/null +++ b/packages/svelte/tests/compiler-errors/samples/const-tag-snippet-invalid-reference-2/_config.js @@ -0,0 +1,10 @@ +import { test } from '../../test'; + +export default test({ + async: true, + error: { + code: 'const_tag_invalid_reference', + message: 'The `{@const foo = ...}` declaration is not available in this snippet ', + position: [298, 301] + } +}); diff --git a/packages/svelte/tests/compiler-errors/samples/const-tag-snippet-invalid-reference-2/main.svelte b/packages/svelte/tests/compiler-errors/samples/const-tag-snippet-invalid-reference-2/main.svelte new file mode 100644 index 0000000000..c59df28ec9 --- /dev/null +++ b/packages/svelte/tests/compiler-errors/samples/const-tag-snippet-invalid-reference-2/main.svelte @@ -0,0 +1,27 @@ + + + + + {@const foo = 'bar'} + {foo} + + + {#snippet prop()} + {foo} + {/snippet} + + + {#snippet prop()} + {@const foo = 'bar'} + {foo} + {/snippet} + + + + + {@const foo = 'bar'} + + {#snippet prop()} + {foo} + {/snippet} + diff --git a/packages/svelte/tests/compiler-errors/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/compiler-errors/test.ts b/packages/svelte/tests/compiler-errors/test.ts index 13b9280dde..b3a2d4af31 100644 --- a/packages/svelte/tests/compiler-errors/test.ts +++ b/packages/svelte/tests/compiler-errors/test.ts @@ -5,6 +5,7 @@ import { suite, type BaseTest } from '../suite'; import { read_file } from '../helpers.js'; interface CompilerErrorTest extends BaseTest { + async?: boolean; error: { code: string; message: string; @@ -29,7 +30,8 @@ const { test, run } = suite((config, cwd) => { try { compile(read_file(`${cwd}/main.svelte`), { - generate: 'client' + generate: 'client', + experimental: { async: config.async ?? false } }); } catch (e) { const error = e as CompileError; diff --git a/packages/svelte/tests/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 @@ + +

nested

\ No newline at end of file diff --git a/packages/svelte/tests/hydration/samples/whitespace-at-block-start/main.svelte b/packages/svelte/tests/hydration/samples/whitespace-at-block-start/main.svelte new file mode 100644 index 0000000000..f1f962b958 --- /dev/null +++ b/packages/svelte/tests/hydration/samples/whitespace-at-block-start/main.svelte @@ -0,0 +1,7 @@ + + +
+ +
\ No newline at end of file diff --git a/packages/svelte/tests/runtime-browser/assert.js b/packages/svelte/tests/runtime-browser/assert.js index 48bde01410..9a294a48c7 100644 --- a/packages/svelte/tests/runtime-browser/assert.js +++ b/packages/svelte/tests/runtime-browser/assert.js @@ -87,15 +87,17 @@ function normalize_html(window, html) { /** @param {any} node */ function normalize_children(node) { // sort attributes - const attributes = Array.from(node.attributes).sort((a, b) => { - return a.name < b.name ? -1 : 1; - }); + const attributes = Array.from(node.attributes).sort( + (/** @type {any} */ a, /** @type {any} */ b) => { + return a.name < b.name ? -1 : 1; + } + ); - attributes.forEach((attr) => { + attributes.forEach((/** @type{any} */ attr) => { node.removeAttribute(attr.name); }); - attributes.forEach((attr) => { + attributes.forEach((/** @type{any} */ attr) => { node.setAttribute(attr.name, attr.value); }); diff --git a/packages/svelte/tests/runtime-browser/custom-elements-samples/host-rune-access-injected-css/_config.js b/packages/svelte/tests/runtime-browser/custom-elements-samples/host-rune-access-injected-css/_config.js new file mode 100644 index 0000000000..99a223492b --- /dev/null +++ b/packages/svelte/tests/runtime-browser/custom-elements-samples/host-rune-access-injected-css/_config.js @@ -0,0 +1,21 @@ +import { test } from '../../assert'; +const tick = () => Promise.resolve(); + +export default test({ + async test({ assert, target }) { + target.innerHTML = ''; + /** @type {any} */ + const el = target.querySelector('custom-element'); + + /** @type {string} */ + let html = ''; + const handle_evt = (e) => (html = e.detail); + el.addEventListener('html', handle_evt); + + await tick(); + await tick(); + await tick(); + + assert.ok(html.includes(' + + + + + + \ No newline at end of file diff --git a/packages/svelte/tests/runtime-legacy/samples/attribute-after-property/_config.js b/packages/svelte/tests/runtime-legacy/samples/attribute-after-property/_config.js new file mode 100644 index 0000000000..f6a98b1797 --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/attribute-after-property/_config.js @@ -0,0 +1,19 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + test({ target, assert }) { + const input = target.querySelector('input'); + const button = target.querySelector('button'); + + assert.equal(input?.step, 'any'); + + button?.click(); + flushSync(); + assert.equal(input?.step, '10'); + + button?.click(); + flushSync(); + assert.equal(input?.step, 'any'); + } +}); diff --git a/packages/svelte/tests/runtime-legacy/samples/attribute-after-property/main.svelte b/packages/svelte/tests/runtime-legacy/samples/attribute-after-property/main.svelte new file mode 100644 index 0000000000..2921e4e241 --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/attribute-after-property/main.svelte @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/packages/svelte/tests/runtime-runes/samples/1000-reading-derived-effects/Component.svelte b/packages/svelte/tests/runtime-runes/samples/1000-reading-derived-effects/Component.svelte new file mode 100644 index 0000000000..7a54323cb9 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/1000-reading-derived-effects/Component.svelte @@ -0,0 +1,7 @@ + \ No newline at end of file diff --git a/packages/svelte/tests/runtime-runes/samples/1000-reading-derived-effects/_config.js b/packages/svelte/tests/runtime-runes/samples/1000-reading-derived-effects/_config.js new file mode 100644 index 0000000000..2e4a27cf09 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/1000-reading-derived-effects/_config.js @@ -0,0 +1,5 @@ +import { test } from '../../test'; + +export default test({ + async test() {} +}); diff --git a/packages/svelte/tests/runtime-runes/samples/1000-reading-derived-effects/main.svelte b/packages/svelte/tests/runtime-runes/samples/1000-reading-derived-effects/main.svelte new file mode 100644 index 0000000000..bd326edfb9 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/1000-reading-derived-effects/main.svelte @@ -0,0 +1,8 @@ + + +{#each arr} + +{/each} diff --git a/packages/svelte/tests/runtime-runes/samples/abort-signal-derived-set-state/_config.js b/packages/svelte/tests/runtime-runes/samples/abort-signal-derived-set-state/_config.js new file mode 100644 index 0000000000..2dacf188d7 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/abort-signal-derived-set-state/_config.js @@ -0,0 +1,12 @@ +import { ok, test } from '../../test'; +import { flushSync } from 'svelte'; + +export default test({ + async test({ assert, target, errors }) { + const btn = target.querySelector('button'); + flushSync(() => { + btn?.click(); + }); + assert.deepEqual(errors, []); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/abort-signal-derived-set-state/main.svelte b/packages/svelte/tests/runtime-runes/samples/abort-signal-derived-set-state/main.svelte new file mode 100644 index 0000000000..ebefe38fb2 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/abort-signal-derived-set-state/main.svelte @@ -0,0 +1,24 @@ + + +{der} + + \ No newline at end of file diff --git a/packages/svelte/tests/runtime-runes/samples/async-await/_config.js b/packages/svelte/tests/runtime-runes/samples/async-await/_config.js new file mode 100644 index 0000000000..dda6a7a895 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-await/_config.js @@ -0,0 +1,43 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + const [reset, one, two, reject] = target.querySelectorAll('button'); + + await tick(); + assert.htmlEqual( + target.innerHTML, + ' waiting' + ); + + one.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ' one_res' + ); + + reset.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ' waiting' + ); + + two.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ' two_res' + ); + + reset.click(); + reject.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ' reject_catch' + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-await/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-await/main.svelte new file mode 100644 index 0000000000..8673e45414 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-await/main.svelte @@ -0,0 +1,22 @@ + + + + + + + + + {#await await deferred.promise + "_res"} + waiting + {:then res} + {res} + {:catch err} + {err}_catch + {/await} + + {#snippet pending()} +

pending

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

0

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

0

`); + assert.equal(input.value, '2'); + + instance.shift(); + await tick(); + assert.htmlEqual(target.innerHTML, `

1

`); + assert.equal(input.value, '2'); + + instance.shift(); + await tick(); + assert.htmlEqual(target.innerHTML, `

2

`); + assert.equal(input.value, '2'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-binding-update-while-focused-2/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-binding-update-while-focused-2/main.svelte new file mode 100644 index 0000000000..2fc898e654 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-binding-update-while-focused-2/main.svelte @@ -0,0 +1,25 @@ + + + + +

{await push(count)}

+ + {#snippet pending()} +

loading...

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

3

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

2

`); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-binding-update-while-focused/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-binding-update-while-focused/main.svelte new file mode 100644 index 0000000000..763ce6ebf0 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-binding-update-while-focused/main.svelte @@ -0,0 +1,27 @@ + + + +

{await value}

+ + + {#snippet pending()} +

loading...

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

Loading...

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

Hello, world!

5 01234`); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-const/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-const/main.svelte new file mode 100644 index 0000000000..7410ff6a6f --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-const/main.svelte @@ -0,0 +1,26 @@ + + + + {@const number = await Promise.resolve(5)} + + {#snippet pending()} +

Loading...

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

{greeting}

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

0

'); + + increment.click(); + await tick(); + assert.deepEqual(logs, [false]); + assert.htmlEqual(target.innerHTML, '

1

'); + + increment.click(); + await tick(); + assert.deepEqual(logs, [false, true]); + assert.htmlEqual(target.innerHTML, '

2

'); + + increment.click(); + await tick(); + assert.deepEqual(logs, [false, true]); + assert.htmlEqual(target.innerHTML, '

3

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

{await count}

+ + {#snippet pending()} +

loading...

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

1

+

1

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

2

+

2

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

{JSON.stringify((await data), null, 2)}

+ {#if true} + +

{unrelated}

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

loading...

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

pending

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

3

'); + + 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/async-template-async-sync-mixed/_config.js b/packages/svelte/tests/runtime-runes/samples/async-template-async-sync-mixed/_config.js new file mode 100644 index 0000000000..709b88578f --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-template-async-sync-mixed/_config.js @@ -0,0 +1,9 @@ +import { tick } from 'svelte'; +import { ok, test } from '../../test'; + +export default test({ + async test({ assert, target }) { + await tick(); + assert.htmlEqual(target.innerHTML, '

foo bar

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

{foo()} {await bar()}

+ + {#snippet pending()} +

pending

+ {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-top-level-error-nested-obsolete/Child.svelte b/packages/svelte/tests/runtime-runes/samples/async-top-level-error-nested-obsolete/Child.svelte new file mode 100644 index 0000000000..f7ba132ace --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-top-level-error-nested-obsolete/Child.svelte @@ -0,0 +1,9 @@ + diff --git a/packages/svelte/tests/runtime-runes/samples/async-top-level-error-nested-obsolete/_config.js b/packages/svelte/tests/runtime-runes/samples/async-top-level-error-nested-obsolete/_config.js new file mode 100644 index 0000000000..298e33e9a2 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-top-level-error-nested-obsolete/_config.js @@ -0,0 +1,15 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: `

pending

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

route: other

'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-top-level-error-nested-obsolete/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-top-level-error-nested-obsolete/main.svelte new file mode 100644 index 0000000000..2f461e96c8 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-top-level-error-nested-obsolete/main.svelte @@ -0,0 +1,18 @@ + + + + + + {#if route.current === 'home'} + + {:else} +

route: {route.current}

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

pending

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

pending

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

failed

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

pending

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

failed

+ {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/binding-update-while-focused-2/_config.js b/packages/svelte/tests/runtime-runes/samples/binding-update-while-focused-2/_config.js new file mode 100644 index 0000000000..7a56c79d71 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/binding-update-while-focused-2/_config.js @@ -0,0 +1,24 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + test({ assert, target }) { + const [input] = target.querySelectorAll('input'); + + input.focus(); + input.value = '3'; + input.dispatchEvent(new InputEvent('input', { bubbles: true })); + flushSync(); + + assert.equal(input.value, '3'); + assert.htmlEqual(target.innerHTML, `

3

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

2

`); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/binding-update-while-focused-2/main.svelte b/packages/svelte/tests/runtime-runes/samples/binding-update-while-focused-2/main.svelte new file mode 100644 index 0000000000..b0597c223b --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/binding-update-while-focused-2/main.svelte @@ -0,0 +1,21 @@ + + +

{value}

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

2

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

{double}

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

2

', + html: '

2

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

{double}

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

{double}

+
diff --git a/packages/svelte/tests/runtime-runes/samples/custom-element-injected-styles/Thing.svelte b/packages/svelte/tests/runtime-runes/samples/custom-element-injected-styles/Thing.svelte new file mode 100644 index 0000000000..0a2b139274 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/custom-element-injected-styles/Thing.svelte @@ -0,0 +1,9 @@ + + +

hello

+ + diff --git a/packages/svelte/tests/runtime-runes/samples/custom-element-injected-styles/_config.js b/packages/svelte/tests/runtime-runes/samples/custom-element-injected-styles/_config.js new file mode 100644 index 0000000000..74597504bd --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/custom-element-injected-styles/_config.js @@ -0,0 +1,15 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + mode: ['client'], + async test({ assert, target }) { + const thing = /** @type HTMLElement & { object: { test: true }; } */ ( + target.querySelector('my-thing') + ); + + await tick(); + + assert.include(thing.shadowRoot?.innerHTML, 'red'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/custom-element-injected-styles/main.svelte b/packages/svelte/tests/runtime-runes/samples/custom-element-injected-styles/main.svelte new file mode 100644 index 0000000000..ba5b788da9 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/custom-element-injected-styles/main.svelte @@ -0,0 +1,5 @@ + + + diff --git a/packages/svelte/tests/runtime-runes/samples/effect-loop-3/Child.svelte b/packages/svelte/tests/runtime-runes/samples/effect-loop-3/Child.svelte new file mode 100644 index 0000000000..9bf4db52d6 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/effect-loop-3/Child.svelte @@ -0,0 +1,13 @@ + + +{#if inited} + {@render children()} +{/if} diff --git a/packages/svelte/tests/runtime-runes/samples/effect-loop-3/_config.js b/packages/svelte/tests/runtime-runes/samples/effect-loop-3/_config.js new file mode 100644 index 0000000000..046c190432 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/effect-loop-3/_config.js @@ -0,0 +1,12 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + const [button] = target.querySelectorAll('button'); + + assert.doesNotThrow(() => { + flushSync(() => button.click()); + }); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/effect-loop-3/main.svelte b/packages/svelte/tests/runtime-runes/samples/effect-loop-3/main.svelte new file mode 100644 index 0000000000..2b3a171798 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/effect-loop-3/main.svelte @@ -0,0 +1,15 @@ + + + + +{#if show} + {#each { length: 1234 } as i} + {i} + {/each} +{/if} diff --git a/packages/svelte/tests/runtime-runes/samples/effect-order-6/A.svelte b/packages/svelte/tests/runtime-runes/samples/effect-order-6/A.svelte index 2e789a0460..22b8ed0f20 100644 --- a/packages/svelte/tests/runtime-runes/samples/effect-order-6/A.svelte +++ b/packages/svelte/tests/runtime-runes/samples/effect-order-6/A.svelte @@ -1,11 +1,12 @@ - + diff --git a/packages/svelte/tests/runtime-runes/samples/effect-order-6/B.svelte b/packages/svelte/tests/runtime-runes/samples/effect-order-6/B.svelte index 1fad19bc15..2233961177 100644 --- a/packages/svelte/tests/runtime-runes/samples/effect-order-6/B.svelte +++ b/packages/svelte/tests/runtime-runes/samples/effect-order-6/B.svelte @@ -1,7 +1,5 @@ - - - -{#if object?.boolean} - - {@render children(object.boolean)} -{/if} diff --git a/packages/svelte/tests/runtime-runes/samples/effect-order-6/_config.js b/packages/svelte/tests/runtime-runes/samples/effect-order-6/_config.js index 8f9077e954..91e1569e46 100644 --- a/packages/svelte/tests/runtime-runes/samples/effect-order-6/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/effect-order-6/_config.js @@ -8,6 +8,6 @@ export default test({ flushSync(() => open.click()); flushSync(() => close.click()); - assert.deepEqual(logs, [true]); + assert.deepEqual(logs, [{ boolean: true, closed: false }]); } }); diff --git a/packages/svelte/tests/runtime-runes/samples/effect-order-6/main.svelte b/packages/svelte/tests/runtime-runes/samples/effect-order-6/main.svelte index eee487fa13..08f6fc48da 100644 --- a/packages/svelte/tests/runtime-runes/samples/effect-order-6/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/effect-order-6/main.svelte @@ -1,6 +1,15 @@ @@ -15,9 +24,6 @@
- - {#snippet children(boolean)} - - {/snippet} - - +{#if object} + +{/if} diff --git a/packages/svelte/tests/runtime-runes/samples/effect-order-7/A.svelte b/packages/svelte/tests/runtime-runes/samples/effect-order-7/A.svelte index 54f4869d62..b55c8f4115 100644 --- a/packages/svelte/tests/runtime-runes/samples/effect-order-7/A.svelte +++ b/packages/svelte/tests/runtime-runes/samples/effect-order-7/A.svelte @@ -1,9 +1,9 @@ -{boolean} +{boolean} {closed} - + diff --git a/packages/svelte/tests/runtime-runes/samples/effect-order-7/B.svelte b/packages/svelte/tests/runtime-runes/samples/effect-order-7/B.svelte index 2a2e634db1..7b33c342f5 100644 --- a/packages/svelte/tests/runtime-runes/samples/effect-order-7/B.svelte +++ b/packages/svelte/tests/runtime-runes/samples/effect-order-7/B.svelte @@ -1,7 +1,5 @@ - - - -{#if object?.nested} - - {@render children(object.nested)} -{/if} diff --git a/packages/svelte/tests/runtime-runes/samples/effect-order-7/main.svelte b/packages/svelte/tests/runtime-runes/samples/effect-order-7/main.svelte index c9c45c50cf..d9e9eb17ad 100644 --- a/packages/svelte/tests/runtime-runes/samples/effect-order-7/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/effect-order-7/main.svelte @@ -1,6 +1,15 @@ @@ -15,8 +24,6 @@
- - {#snippet children(nested)} -
- {/snippet} - +{#if object} + +{/if} diff --git a/packages/svelte/tests/runtime-runes/samples/inspect-recursive-2/main.svelte b/packages/svelte/tests/runtime-runes/samples/inspect-recursive-2/main.svelte index f7874d2192..23a1c237c3 100644 --- a/packages/svelte/tests/runtime-runes/samples/inspect-recursive-2/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/inspect-recursive-2/main.svelte @@ -1,21 +1,15 @@ \ No newline at end of file diff --git a/packages/svelte/tests/runtime-runes/samples/props-default-value-function/_config.js b/packages/svelte/tests/runtime-runes/samples/props-default-value-function/_config.js new file mode 100644 index 0000000000..6b281f04f0 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/props-default-value-function/_config.js @@ -0,0 +1,15 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + mode: ['client'], + + test({ assert, target }) { + const btn = target.querySelector('button'); + + assert.htmlEqual(target.innerHTML, ` Inner: 0 Inner: 0`); + btn?.click(); + flushSync(); + assert.htmlEqual(target.innerHTML, ` Inner: 1 Inner: 1`); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/props-default-value-function/inner.svelte b/packages/svelte/tests/runtime-runes/samples/props-default-value-function/inner.svelte new file mode 100644 index 0000000000..6bde0a15a8 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/props-default-value-function/inner.svelte @@ -0,0 +1,4 @@ + +Inner: {getter()} diff --git a/packages/svelte/tests/runtime-runes/samples/props-default-value-function/main.svelte b/packages/svelte/tests/runtime-runes/samples/props-default-value-function/main.svelte new file mode 100644 index 0000000000..2cb2f67b82 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/props-default-value-function/main.svelte @@ -0,0 +1,14 @@ + + + + + \ No newline at end of file diff --git a/packages/svelte/tests/runtime-runes/samples/props-default-value-function/wrapper.svelte b/packages/svelte/tests/runtime-runes/samples/props-default-value-function/wrapper.svelte new file mode 100644 index 0000000000..525494ddfb --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/props-default-value-function/wrapper.svelte @@ -0,0 +1,7 @@ + + + diff --git a/packages/svelte/tests/runtime-runes/samples/props-default-value-function/wrapper2.svelte b/packages/svelte/tests/runtime-runes/samples/props-default-value-function/wrapper2.svelte new file mode 100644 index 0000000000..9498f432d8 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/props-default-value-function/wrapper2.svelte @@ -0,0 +1,7 @@ + + + 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/signals/test.ts b/packages/svelte/tests/signals/test.ts index 937324727b..eff6d6166a 100644 --- a/packages/svelte/tests/signals/test.ts +++ b/packages/svelte/tests/signals/test.ts @@ -1390,4 +1390,55 @@ describe('signals', () => { destroy(); }; }); + + test('$effect.root inside deriveds stay alive independently', () => { + const log: any[] = []; + const c = state(0); + const cleanup: any[] = []; + const inner_states: any[] = []; + + const d = derived(() => { + const destroy = effect_root(() => { + const x = state(0); + inner_states.push(x); + + effect(() => { + log.push('inner ' + $.get(x)); + return () => { + log.push('inner destroyed'); + }; + }); + }); + + cleanup.push(destroy); + + return $.get(c); + }); + + return () => { + log.push($.get(d)); + flushSync(); + + assert.deepEqual(log, [0, 'inner 0']); + log.length = 0; + + set(inner_states[0], 1); + flushSync(); + + assert.deepEqual(log, ['inner destroyed', 'inner 1']); + log.length = 0; + + set(c, 1); + log.push($.get(d)); + flushSync(); + + assert.deepEqual(log, [1, 'inner 0']); + log.length = 0; + + cleanup.forEach((fn) => fn()); + flushSync(); + + assert.deepEqual(log, ['inner destroyed', 'inner destroyed']); + }; + }); }); diff --git a/packages/svelte/tests/validator/samples/class-state-constructor-9/errors.json b/packages/svelte/tests/validator/samples/class-state-constructor-9/errors.json index b7dd4c8ed4..94b5f191c2 100644 --- a/packages/svelte/tests/validator/samples/class-state-constructor-9/errors.json +++ b/packages/svelte/tests/validator/samples/class-state-constructor-9/errors.json @@ -1,14 +1,14 @@ [ { - "code": "state_field_invalid_assignment", - "message": "Cannot assign to a state field before its declaration", + "code": "duplicate_class_field", + "message": "`count` has already been declared", "start": { - "line": 2, - "column": 1 + "line": 5, + "column": 2 }, "end": { - "line": 2, - "column": 12 + "line": 5, + "column": 24 } } ] 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/tests/validator/samples/const-tag-placement-svelte-boundary/input.svelte b/packages/svelte/tests/validator/samples/const-tag-placement-svelte-boundary/input.svelte index 5708cc36ca..c965a379e5 100644 --- a/packages/svelte/tests/validator/samples/const-tag-placement-svelte-boundary/input.svelte +++ b/packages/svelte/tests/validator/samples/const-tag-placement-svelte-boundary/input.svelte @@ -4,8 +4,6 @@ {@const x = a} - {#snippet failed()} - {x} - {/snippet} + {x} - \ No newline at end of file +
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': {}