Merge branch 'main' into effect-active

effect-active
ComputerGuy 7 days ago committed by GitHub
commit dd46f3c6eb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -1,5 +0,0 @@
---
'svelte': patch
---
fix: don't reexecute derived with no dependencies on teardown

@ -60,6 +60,23 @@ jobs:
env: env:
CI: true CI: true
SVELTE_NO_ASYNC: 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: Lint:
permissions: {} permissions: {}
runs-on: ubuntu-latest runs-on: ubuntu-latest

@ -101,13 +101,13 @@ Test samples are kept in `/test/xxx/samples` folder.
1. To run test, run `pnpm test`. 1. To run test, run `pnpm test`.
1. To run a particular test suite, use `pnpm test <suite-name>`, for example: 1. To run a particular test suite, use `pnpm test <suite-name>`, for example:
```bash ```sh
pnpm test validator pnpm test validator
``` ```
1. To filter tests _within_ a test suite, use `pnpm test <suite-name> -t <test-name>`, for example: 1. To filter tests _within_ a test suite, use `pnpm test <suite-name> -t <test-name>`, for example:
```bash ```sh
pnpm test validator -t a11y-alt-text pnpm test validator -t a11y-alt-text
``` ```

@ -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: 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 npx sv create myapp
cd myapp cd myapp
npm install npm install

@ -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. 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. Conversely, if `condition` is `false`, `color` will not be evaluated, and the effect will _only_ re-run again when `condition` changes.

@ -277,4 +277,4 @@ Snippets can be created programmatically with the [`createRawSnippet`](svelte#cr
## Snippets and slots ## 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.

@ -22,7 +22,7 @@ It also will not compile Svelte code.
## Styling ## 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:
<!-- prettier-ignore --> <!-- prettier-ignore -->
```svelte ```svelte

@ -71,7 +71,7 @@ The user of this component has the same flexibility to use a mixture of objects,
</Button> </Button>
``` ```
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 ```svelte
<script lang="ts"> <script lang="ts">

@ -12,7 +12,7 @@ The `<svelte:options>` element provides a place to specify per-component compile
- `runes={false}` — forces a component into _legacy mode_ - `runes={false}` — forces a component into _legacy mode_
- `namespace="..."` — the namespace where this component will be used, can be "html" (the default), "svg" or "mathml" - `namespace="..."` — the namespace where this component will be used, can be "html" (the default), "svg" or "mathml"
- `customElement={...}` — the [options](custom-elements#Component-options) to use when compiling this component as a custom element. If a string is passed, it is used as the `tag` option - `customElement={...}` — the [options](custom-elements#Component-options) to use when compiling this component as a custom element. If a string is passed, it is used as the `tag` option
- `css="injected"` — the component will inject its styles inline: During server side rendering, it's injected as a `<style>` tag in the `head`, during client side rendering, it's loaded via JavaScript - `css="injected"` — the component will inject its styles inline: During server-side rendering, it's injected as a `<style>` tag in the `head`, during client side rendering, it's loaded via JavaScript
> [!LEGACY] Deprecated options > [!LEGACY] Deprecated options
> Svelte 4 also included the following options. They are deprecated in Svelte 5 and non-functional in runes mode. > Svelte 4 also included the following options. They are deprecated in Svelte 5 and non-functional in runes mode.

@ -41,7 +41,7 @@ If a function is returned from `onMount`, it will be called when the component i
</script> </script>
``` ```
> [!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` ## `onDestroy`

@ -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: To setup Vitest manually, first install it:
```bash ```sh
npm install -D vitest 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): To get started, install jsdom (a library that shims DOM APIs):
```bash ```sh
npm install -D jsdom npm install -D jsdom
``` ```

@ -245,7 +245,7 @@ In Svelte 4, you can add event modifiers to handlers:
<button on:click|once|preventDefault={handler}>...</button> <button on:click|once|preventDefault={handler}>...</button>
``` ```
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: 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 ## 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: 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 ### 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 ```js
+++import { render } from 'svelte/server';+++ +++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 ### 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 ```svelte
<table> <table>
@ -835,7 +835,7 @@ Assignments to destructured parts of a `@const` declaration are no longer allowe
### :is(...), :has(...), and :where(...) are scoped ### :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: 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 ### 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 ### `onevent` attributes are delegated

@ -196,6 +196,51 @@ Cyclical dependency detected: %cycle%
`{@const}` must be the immediate child of `{#snippet}`, `{#if}`, `{:else if}`, `{:else}`, `{#each}`, `{:then}`, `{:catch}`, `<svelte:fragment>`, `<svelte:boundary` or `<Component>` `{@const}` must be the immediate child of `{#snippet}`, `{#if}`, `{:else if}`, `{:else}`, `{#each}`, `{:then}`, `{:catch}`, `<svelte:fragment>`, `<svelte:boundary` or `<Component>`
``` ```
### const_tag_invalid_reference
```
The `{@const %name% = ...}` declaration is not available in this snippet
```
The following is an error:
```svelte
<svelte:boundary>
{@const foo = 'bar'}
{#snippet failed()}
{foo}
{/snippet}
</svelte:boundary>
```
Here, `foo` is not available inside `failed`. The top level code inside `<svelte:boundary>` becomes part of the implicit `children` snippet, in other words the above code is equivalent to this:
```svelte
<svelte:boundary>
{#snippet children()}
{@const foo = 'bar'}
{/snippet}
{#snippet failed()}
{foo}
{/snippet}
</svelte:boundary>
```
The same applies to components:
```svelte
<Component>
{@const foo = 'bar'}
{#snippet someProp()}
<!-- error -->
{foo}
{/snippet}
</Component>
```
### constant_assignment ### 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 The $ prefix is reserved, and cannot be used for variables and imports
``` ```
### duplicate_class_field
```
`%name%` has already been declared
```
### each_item_invalid_assignment ### each_item_invalid_assignment
``` ```

@ -679,11 +679,11 @@ In HTML, there's [no such thing as a self-closing tag](https://jakearchibald.com
</div> </div>
``` ```
Some templating languages (including Svelte) will 'fix' HTML by turning `<span />` into `<span></span>`. 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 `<span />` into `<span></span>`. 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: To automate this, run the dedicated migration:
```bash ```sh
npx sv migrate self-closing-tags npx sv migrate self-closing-tags
``` ```

@ -2,4 +2,6 @@
title: svelte/action title: svelte/action
--- ---
This module provides types for [actions](use), which have been superseded by [attachments](@attach).
> MODULE: svelte/action > MODULE: svelte/action

@ -1,5 +1,197 @@
# svelte # svelte
## 5.38.6
### Patch Changes
- fix: don't fail on `flushSync` while flushing effects ([#16674](https://github.com/sveltejs/svelte/pull/16674))
## 5.38.5
### Patch Changes
- fix: ensure async deriveds always get dependencies from thennable ([#16672](https://github.com/sveltejs/svelte/pull/16672))
## 5.38.4
### Patch Changes
- fix: place instance-level snippets inside async body ([#16666](https://github.com/sveltejs/svelte/pull/16666))
- fix: Add check for builtin custom elements in `set_custom_element_data` ([#16592](https://github.com/sveltejs/svelte/pull/16592))
- fix: restore batch along with effect context ([#16668](https://github.com/sveltejs/svelte/pull/16668))
- fix: wait until changes propagate before updating input selection state ([#16649](https://github.com/sveltejs/svelte/pull/16649))
- fix: add "Accept-CH" as valid value for `http-equiv` ([#16671](https://github.com/sveltejs/svelte/pull/16671))
## 5.38.3
### Patch Changes
- fix: ensure correct order of template effect values ([#16655](https://github.com/sveltejs/svelte/pull/16655))
- fix: allow async `{@const}` in more places ([#16643](https://github.com/sveltejs/svelte/pull/16643))
- fix: properly catch top level await errors ([#16619](https://github.com/sveltejs/svelte/pull/16619))
- perf: prune effects without dependencies ([#16625](https://github.com/sveltejs/svelte/pull/16625))
- fix: only emit `for_await_track_reactivity_loss` in async mode ([#16644](https://github.com/sveltejs/svelte/pull/16644))
## 5.38.2
### Patch Changes
- 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
- fix: ensure subscriptions are picked up correctly by deriveds ([#16466](https://github.com/sveltejs/svelte/pull/16466))
## 5.36.12
### Patch Changes
- chore: move `capture_signals` to legacy module ([#16456](https://github.com/sveltejs/svelte/pull/16456))
## 5.36.11
### Patch Changes
- fix: always mark reactions of deriveds ([#16457](https://github.com/sveltejs/svelte/pull/16457))
- fix: add labels to `@const` tags and props ([#16454](https://github.com/sveltejs/svelte/pull/16454))
- fix: tag stores for `$inspect.trace()` ([#16452](https://github.com/sveltejs/svelte/pull/16452))
## 5.36.10
### Patch Changes
- fix: prevent batches from getting intertwined ([#16446](https://github.com/sveltejs/svelte/pull/16446))
## 5.36.9
### Patch Changes
- fix: don't reexecute derived with no dependencies on teardown ([#16438](https://github.com/sveltejs/svelte/pull/16438))
- fix: disallow `export { foo as default }` in `<script module>` ([#16447](https://github.com/sveltejs/svelte/pull/16447))
- fix: move ownership validation into async component body ([#16449](https://github.com/sveltejs/svelte/pull/16449))
- fix: allow async destructured deriveds ([#16444](https://github.com/sveltejs/svelte/pull/16444))
- fix: move store setup/cleanup outside of async component body ([#16443](https://github.com/sveltejs/svelte/pull/16443))
## 5.36.8 ## 5.36.8
### Patch Changes ### Patch Changes

@ -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): 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 npx sv create my-app
cd my-app cd my-app
npm install npm install
@ -30,7 +30,7 @@ See [the SvelteKit documentation](https://svelte.dev/docs/kit) to learn more.
## Changelog ## 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 ## Supporting Svelte

@ -464,6 +464,14 @@ export interface DOMAttributes<T extends EventTarget> {
onfullscreenerror?: EventHandler<Event, T> | undefined | null; onfullscreenerror?: EventHandler<Event, T> | undefined | null;
onfullscreenerrorcapture?: EventHandler<Event, T> | undefined | null; onfullscreenerrorcapture?: EventHandler<Event, T> | undefined | null;
// Dimensions
readonly 'bind:contentRect'?: DOMRectReadOnly | undefined | null;
readonly 'bind:contentBoxSize'?: Array<ResizeObserverSize> | undefined | null;
readonly 'bind:borderBoxSize'?: Array<ResizeObserverSize> | undefined | null;
readonly 'bind:devicePixelContentBoxSize'?: Array<ResizeObserverSize> | undefined | null;
readonly 'bind:clientWidth'?: number | undefined | null;
readonly 'bind:clientHeight'?: number | undefined | null;
xmlns?: string | undefined | null; xmlns?: string | undefined | null;
} }
@ -773,7 +781,7 @@ export interface HTMLAttributes<T extends EventTarget> extends AriaAttributes, D
title?: string | undefined | null; title?: string | undefined | null;
translate?: 'yes' | 'no' | '' | undefined | null; translate?: 'yes' | 'no' | '' | undefined | null;
inert?: boolean | undefined | null; inert?: boolean | undefined | null;
popover?: 'auto' | 'manual' | '' | undefined | null; popover?: 'auto' | 'manual' | 'hint' | '' | undefined | null;
writingsuggestions?: Booleanish | undefined | null; writingsuggestions?: Booleanish | undefined | null;
// Unknown // Unknown
@ -839,13 +847,7 @@ export interface HTMLAttributes<T extends EventTarget> extends AriaAttributes, D
*/ */
'bind:innerText'?: string | undefined | null; 'bind:innerText'?: string | undefined | null;
readonly 'bind:contentRect'?: DOMRectReadOnly | undefined | null;
readonly 'bind:contentBoxSize'?: Array<ResizeObserverSize> | undefined | null;
readonly 'bind:borderBoxSize'?: Array<ResizeObserverSize> | undefined | null;
readonly 'bind:devicePixelContentBoxSize'?: Array<ResizeObserverSize> | undefined | null;
readonly 'bind:focused'?: boolean | 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:offsetWidth'?: number | undefined | null;
readonly 'bind:offsetHeight'?: number | undefined | null; readonly 'bind:offsetHeight'?: number | undefined | null;
@ -996,7 +998,7 @@ export interface HTMLFieldsetAttributes extends HTMLAttributes<HTMLFieldSetEleme
} }
export interface HTMLFormAttributes extends HTMLAttributes<HTMLFormElement> { export interface HTMLFormAttributes extends HTMLAttributes<HTMLFormElement> {
acceptcharset?: string | undefined | null; 'accept-charset'?: 'utf-8' | (string & {}) | undefined | null;
action?: string | undefined | null; action?: string | undefined | null;
autocomplete?: AutoFillBase | undefined | null; autocomplete?: AutoFillBase | undefined | null;
enctype?: enctype?:
@ -1266,6 +1268,7 @@ export interface HTMLMetaAttributes extends HTMLAttributes<HTMLMetaElement> {
charset?: string | undefined | null; charset?: string | undefined | null;
content?: string | undefined | null; content?: string | undefined | null;
'http-equiv'?: 'http-equiv'?:
| 'accept-ch'
| 'content-security-policy' | 'content-security-policy'
| 'content-type' | 'content-type'
| 'default-style' | 'default-style'
@ -1553,6 +1556,7 @@ export interface SVGAttributes<T extends EventTarget> extends AriaAttributes, DO
height?: number | string | undefined | null; height?: number | string | undefined | null;
id?: string | undefined | null; id?: string | undefined | null;
lang?: string | undefined | null; lang?: string | undefined | null;
part?: string | undefined | null;
max?: number | string | undefined | null; max?: number | string | undefined | null;
media?: string | undefined | null; media?: string | undefined | null;
// On the `textPath` element // On the `textPath` element

@ -30,6 +30,10 @@
> The $ prefix 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 ## 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}`) > 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}`)

@ -124,6 +124,49 @@
> `{@const}` must be the immediate child of `{#snippet}`, `{#if}`, `{:else if}`, `{:else}`, `{#each}`, `{:then}`, `{:catch}`, `<svelte:fragment>`, `<svelte:boundary` or `<Component>` > `{@const}` must be the immediate child of `{#snippet}`, `{#if}`, `{:else if}`, `{:else}`, `{#each}`, `{:then}`, `{:catch}`, `<svelte:fragment>`, `<svelte:boundary` or `<Component>`
## const_tag_invalid_reference
> The `{@const %name% = ...}` declaration is not available in this snippet
The following is an error:
```svelte
<svelte:boundary>
{@const foo = 'bar'}
{#snippet failed()}
{foo}
{/snippet}
</svelte:boundary>
```
Here, `foo` is not available inside `failed`. The top level code inside `<svelte:boundary>` becomes part of the implicit `children` snippet, in other words the above code is equivalent to this:
```svelte
<svelte:boundary>
{#snippet children()}
{@const foo = 'bar'}
{/snippet}
{#snippet failed()}
{foo}
{/snippet}
</svelte:boundary>
```
The same applies to components:
```svelte
<Component>
{@const foo = 'bar'}
{#snippet someProp()}
<!-- error -->
{foo}
{/snippet}
</Component>
```
## debug_tag_invalid_arguments ## debug_tag_invalid_arguments
> {@debug ...} arguments must be identifiers, not arbitrary expressions > {@debug ...} arguments must be identifiers, not arbitrary expressions

@ -67,11 +67,11 @@ In HTML, there's [no such thing as a self-closing tag](https://jakearchibald.com
</div> </div>
``` ```
Some templating languages (including Svelte) will 'fix' HTML by turning `<span />` into `<span></span>`. 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 `<span />` into `<span></span>`. 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: To automate this, run the dedicated migration:
```bash ```sh
npx sv migrate self-closing-tags npx sv migrate self-closing-tags
``` ```

@ -2,7 +2,7 @@
"name": "svelte", "name": "svelte",
"description": "Cybernetically enhanced web apps", "description": "Cybernetically enhanced web apps",
"license": "MIT", "license": "MIT",
"version": "5.36.8", "version": "5.38.6",
"type": "module", "type": "module",
"types": "./types/index.d.ts", "types": "./types/index.d.ts",
"engines": { "engines": {
@ -141,6 +141,7 @@
"build": "node scripts/process-messages && rollup -c && pnpm generate:types && node scripts/check-treeshakeability.js", "build": "node scripts/process-messages && rollup -c && pnpm generate:types && node scripts/check-treeshakeability.js",
"dev": "node scripts/process-messages -w & rollup -cw", "dev": "node scripts/process-messages -w & rollup -cw",
"check": "tsc --project tsconfig.runtime.json && tsc && cd ./tests/types && tsc", "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", "check:watch": "tsc --watch",
"generate:version": "node ./scripts/generate-version.js", "generate:version": "node ./scripts/generate-version.js",
"generate:types": "node ./scripts/generate-types.js && tsc -p tsconfig.generated.json", "generate:types": "node ./scripts/generate-types.js && tsc -p tsconfig.generated.json",
@ -165,7 +166,7 @@
"vitest": "^2.1.9" "vitest": "^2.1.9"
}, },
"dependencies": { "dependencies": {
"@ampproject/remapping": "^2.3.0", "@jridgewell/remapping": "^2.3.4",
"@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/sourcemap-codec": "^1.5.0",
"@sveltejs/acorn-typescript": "^1.0.5", "@sveltejs/acorn-typescript": "^1.0.5",
"@types/estree": "^1.0.5", "@types/estree": "^1.0.5",

@ -26,9 +26,11 @@ await createBundle({
// so that types/properties with `@internal` (and its dependencies) are removed from the output // so that types/properties with `@internal` (and its dependencies) are removed from the output
stripInternal: true, stripInternal: true,
paths: Object.fromEntries( paths: Object.fromEntries(
Object.entries(pkg.imports).map(([key, value]) => { Object.entries(pkg.imports).map(
return [key, [value.types ?? value.default ?? value]]; /** @param {[string,any]} import */ ([key, value]) => {
}) return [key, [value.types ?? value.default ?? value]];
}
)
) )
}, },
modules: { modules: {

@ -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. * 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 * https://svelte.dev/docs/svelte/$effect
* @param fn The function to execute * @param fn The function to execute
@ -276,7 +276,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. * 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 * https://svelte.dev/docs/svelte/$effect#$effect.pre
* @param fn The function to execute * @param fn The function to execute

@ -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`); 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}`) * 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 * @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}\`, \`<svelte:fragment>\`, \`<svelte:boundary\` or \`<Component>\`\nhttps://svelte.dev/e/const_tag_invalid_placement`); e(node, 'const_tag_invalid_placement', `\`{@const}\` must be the immediate child of \`{#snippet}\`, \`{#if}\`, \`{:else if}\`, \`{:else}\`, \`{#each}\`, \`{:then}\`, \`{:catch}\`, \`<svelte:fragment>\`, \`<svelte:boundary\` or \`<Component>\`\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 * {@debug ...} arguments must be identifiers, not arbitrary expressions
* @param {null | number | NodeLike} node * @param {null | number | NodeLike} node

@ -55,7 +55,9 @@ export function convert(source, ast) {
// Insert svelte:options back into the root nodes // Insert svelte:options back into the root nodes
if (/** @type {any} */ (options)?.__raw__) { 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) { if (idx === -1) {
idx = node.fragment.nodes.length; idx = node.fragment.nodes.length;
} }

@ -1707,14 +1707,14 @@ function extract_type_and_comment(declarator, state, path) {
} }
// Ensure modifiers are applied in the same order as Svelte 4 // Ensure modifiers are applied in the same order as Svelte 4
const modifier_order = [ const modifier_order = /** @type {const} */ ([
'preventDefault', 'preventDefault',
'stopPropagation', 'stopPropagation',
'stopImmediatePropagation', 'stopImmediatePropagation',
'self', 'self',
'trusted', 'trusted',
'once' 'once'
]; ]);
/** /**
* @param {AST.RegularElement | AST.SvelteElement | AST.SvelteWindow | AST.SvelteDocument | AST.SvelteBody} element * @param {AST.RegularElement | AST.SvelteElement | AST.SvelteWindow | AST.SvelteDocument | AST.SvelteBody} element

@ -1,5 +1,4 @@
/** @import { AST } from '#compiler' */ /** @import { AST } from '#compiler' */
/** @import { Comment } from 'estree' */
// @ts-expect-error acorn type definitions are borked in the release we use // @ts-expect-error acorn type definitions are borked in the release we use
import { isIdentifierStart, isIdentifierChar } from 'acorn'; import { isIdentifierStart, isIdentifierChar } from 'acorn';
import fragment from './state/fragment.js'; import fragment from './state/fragment.js';
@ -23,12 +22,6 @@ export class Parser {
*/ */
template; template;
/**
* @readonly
* @type {string}
*/
template_untrimmed;
/** /**
* Whether or not we're in loose parsing mode, in which * Whether or not we're in loose parsing mode, in which
* case we try to continue parsing as much as possible * case we try to continue parsing as much as possible
@ -67,7 +60,6 @@ export class Parser {
} }
this.loose = loose; this.loose = loose;
this.template_untrimmed = template;
this.template = template.trimEnd(); this.template = template.trimEnd();
let match_lang; let match_lang;

@ -370,14 +370,6 @@ export default function element(parser) {
// ... or we're followed by whitespace, for example near the end of the template, // ... 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 // which we want to take in so that language tools has more room to work with
parser.allow_whitespace(); 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++;
}
}
} }
} }
} }

@ -10,7 +10,8 @@ export function create_fragment(transparent = false) {
nodes: [], nodes: [],
metadata: { metadata: {
transparent, transparent,
dynamic: false dynamic: false,
has_await: false
} }
}; };
} }

@ -9,8 +9,8 @@ import {
import { regex_ends_with_whitespace, regex_starts_with_whitespace } from '../../patterns.js'; import { regex_ends_with_whitespace, regex_starts_with_whitespace } from '../../patterns.js';
import { get_attribute_chunks, is_text_attribute } from '../../../utils/ast.js'; import { get_attribute_chunks, is_text_attribute } from '../../../utils/ast.js';
/** @typedef {NODE_PROBABLY_EXISTS | NODE_DEFINITELY_EXISTS} NodeExistsValue */ /** @typedef {typeof NODE_PROBABLY_EXISTS | typeof NODE_DEFINITELY_EXISTS} NodeExistsValue */
/** @typedef {FORWARD | BACKWARD} Direction */ /** @typedef {typeof FORWARD | typeof BACKWARD} Direction */
const NODE_PROBABLY_EXISTS = 0; const NODE_PROBABLY_EXISTS = 0;
const NODE_DEFINITELY_EXISTS = 1; const NODE_DEFINITELY_EXISTS = 1;

@ -37,6 +37,7 @@ import { ExportNamedDeclaration } from './visitors/ExportNamedDeclaration.js';
import { ExportSpecifier } from './visitors/ExportSpecifier.js'; import { ExportSpecifier } from './visitors/ExportSpecifier.js';
import { ExpressionStatement } from './visitors/ExpressionStatement.js'; import { ExpressionStatement } from './visitors/ExpressionStatement.js';
import { ExpressionTag } from './visitors/ExpressionTag.js'; import { ExpressionTag } from './visitors/ExpressionTag.js';
import { Fragment } from './visitors/Fragment.js';
import { FunctionDeclaration } from './visitors/FunctionDeclaration.js'; import { FunctionDeclaration } from './visitors/FunctionDeclaration.js';
import { FunctionExpression } from './visitors/FunctionExpression.js'; import { FunctionExpression } from './visitors/FunctionExpression.js';
import { HtmlTag } from './visitors/HtmlTag.js'; import { HtmlTag } from './visitors/HtmlTag.js';
@ -156,6 +157,7 @@ const visitors = {
ExportSpecifier, ExportSpecifier,
ExpressionStatement, ExpressionStatement,
ExpressionTag, ExpressionTag,
Fragment,
FunctionDeclaration, FunctionDeclaration,
FunctionExpression, FunctionExpression,
HtmlTag, 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, // 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 // 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), ast_type: /** @type {any} */ (null),
component_slots: new Set(), component_slots: /** @type {Set<string>} */ (new Set()),
expression: null, expression: null,
function_depth: 0, function_depth: 0,
has_props_rune: false, has_props_rune: false,
options: /** @type {ValidatedCompileOptions} */ (options), options: /** @type {ValidatedCompileOptions} */ (options),
fragment: null,
parent_element: null, parent_element: null,
reactive_statement: 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 // TODO remove all the ?? stuff, we don't need it now that we're validating the config
/** @type {ComponentAnalysis} */ /** @type {ComponentAnalysis} */
const analysis = { const analysis = {
@ -500,13 +505,13 @@ export function analyze_component(root, source, options) {
needs_props: false, needs_props: false,
event_directive_node: null, event_directive_node: null,
uses_event_attributes: false, uses_event_attributes: false,
custom_element: options.customElementOptions ?? options.customElement, custom_element: is_custom_element,
inject_styles: options.css === 'injected' || options.customElement, inject_styles: options.css === 'injected' || is_custom_element,
accessors: options.customElement accessors:
? true is_custom_element ||
: (runes ? false : !!options.accessors) || (runes ? false : !!options.accessors) ||
// because $set method needs accessors // because $set method needs accessors
options.compatibility?.componentApi === 4, options.compatibility?.componentApi === 4,
reactive_statements: new Map(), reactive_statements: new Map(),
binding_groups: new Map(), binding_groups: new Map(),
slot_names: new Map(), slot_names: new Map(),
@ -524,7 +529,6 @@ export function analyze_component(root, source, options) {
has_global: false has_global: false
}, },
source, source,
undefined_exports: new Map(),
snippet_renderers: new Map(), snippet_renderers: new Map(),
snippets: new Set(), snippets: new Set(),
async_deriveds: new Set() async_deriveds: new Set()
@ -686,6 +690,7 @@ export function analyze_component(root, source, options) {
analysis, analysis,
options, options,
ast_type: ast === instance.ast ? 'instance' : ast === template.ast ? 'template' : 'module', ast_type: ast === instance.ast ? 'instance' : ast === template.ast ? 'template' : 'module',
fragment: ast === template.ast ? ast : null,
parent_element: null, parent_element: null,
has_props_rune: false, has_props_rune: false,
component_slots: new Set(), component_slots: new Set(),
@ -751,6 +756,7 @@ export function analyze_component(root, source, options) {
scopes, scopes,
analysis, analysis,
options, options,
fragment: ast === template.ast ? ast : null,
parent_element: null, parent_element: null,
has_props_rune: false, has_props_rune: false,
ast_type: ast === instance.ast ? 'instance' : ast === template.ast ? 'template' : 'module', 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) { if (node.type === 'ExportNamedDeclaration' && node.specifiers !== null && node.source == null) {
for (const specifier of node.specifiers) { for (const specifier of node.specifiers) {
if (specifier.local.type !== 'Identifier') continue; if (specifier.local.type !== 'Identifier') continue;
const name = specifier.local.name;
const binding = analysis.module.scope.get(specifier.local.name); const binding = analysis.module.scope.get(name);
if (!binding) e.export_undefined(specifier, specifier.local.name); if (!binding) {
if ([...analysis.snippets].find((snippet) => snippet.expression.name === name)) {
e.snippet_invalid_export(specifier);
} else {
e.export_undefined(specifier, name);
}
}
} }
} }
} }

@ -8,6 +8,7 @@ export interface AnalysisState {
analysis: ComponentAnalysis; analysis: ComponentAnalysis;
options: ValidatedCompileOptions; options: ValidatedCompileOptions;
ast_type: 'instance' | 'template' | 'module'; 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. * 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. * Parent doesn't necessarily mean direct path predecessor because there could be `#each`, `#if` etc in-between.

@ -14,6 +14,7 @@ export default function check_graph_for_cycles(edges) {
}, new Map()); }, new Map());
const visited = new Set(); const visited = new Set();
/** @type {Set<T>} */
const on_stack = new Set(); const on_stack = new Set();
/** @type {Array<Array<T>>} */ /** @type {Array<Array<T>>} */
const cycles = []; const cycles = [];

@ -11,6 +11,15 @@ export function AwaitExpression(node, context) {
if (context.state.expression) { if (context.state.expression) {
context.state.expression.has_await = true; 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; suspend = true;
} }

@ -33,6 +33,9 @@ export function ClassBody(node, context) {
/** @type {Map<string, StateField>} */ /** @type {Map<string, StateField>} */
const state_fields = new Map(); const state_fields = new Map();
/** @type {Map<string, Array<MethodDefinition['kind'] | 'prop' | 'assigned_prop'>>} */
const fields = new Map();
context.state.analysis.classes.set(node, state_fields); context.state.analysis.classes.set(node, state_fields);
/** @type {MethodDefinition | null} */ /** @type {MethodDefinition | null} */
@ -54,6 +57,14 @@ export function ClassBody(node, context) {
e.state_field_duplicate(node, name); 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, { state_fields.set(name, {
node, node,
type: rune, type: rune,
@ -67,10 +78,48 @@ export function ClassBody(node, context) {
for (const child of node.body) { for (const child of node.body) {
if (child.type === 'PropertyDefinition' && !child.computed && !child.static) { if (child.type === 'PropertyDefinition' && !child.computed && !child.static) {
handle(child, child.key, child.value); 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') { if (child.type === 'MethodDefinition') {
constructor = child; 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);
}
} }
} }

@ -11,6 +11,17 @@ export function ExportNamedDeclaration(node, context) {
// visit children, so bindings are correctly initialised // visit children, so bindings are correctly initialised
context.next(); context.next();
if (
context.state.ast_type &&
node.specifiers.some((specifier) =>
specifier.exported.type === 'Identifier'
? specifier.exported.name === 'default'
: specifier.exported.value === 'default'
)
) {
e.module_illegal_default_export(node);
}
if (node.declaration?.type === 'VariableDeclaration') { if (node.declaration?.type === 'VariableDeclaration') {
// in runes mode, forbid `export let` // in runes mode, forbid `export let`
if ( if (

@ -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 });
}

@ -7,6 +7,7 @@ import * as w from '../../../warnings.js';
import { is_rune } from '../../../../utils.js'; import { is_rune } from '../../../../utils.js';
import { mark_subtree_dynamic } from './shared/fragment.js'; import { mark_subtree_dynamic } from './shared/fragment.js';
import { get_rune } from '../../scope.js'; import { get_rune } from '../../scope.js';
import { is_component_node } from '../../nodes.js';
/** /**
* @param {Identifier} node * @param {Identifier} node
@ -93,7 +94,10 @@ export function Identifier(node, context) {
context.state.expression.references.add(binding); context.state.expression.references.add(binding);
context.state.expression.has_state ||= context.state.expression.has_state ||=
binding.kind !== 'static' && 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; !context.state.scope.evaluate(node).is_known;
} }
@ -152,5 +156,37 @@ export function Identifier(node, context) {
) { ) {
w.reactive_declaration_module_script_dependency(node); 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;
}
}
}
}
} }
} }

@ -35,11 +35,6 @@ export function SnippetBlock(node, context) {
if (can_hoist) { if (can_hoist) {
const binding = /** @type {Binding} */ (context.state.scope.get(name)); const binding = /** @type {Binding} */ (context.state.scope.get(name));
context.state.analysis.module.scope.declarations.set(name, binding); 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; node.metadata.can_hoist = can_hoist;

@ -599,7 +599,7 @@ function has_disabled_attribute(attribute_map) {
/** /**
* @param {string} tag_name * @param {string} tag_name
* @param {Map<string, AST.Attribute>} attribute_map * @param {Map<string, AST.Attribute>} attribute_map
* @returns {ElementInteractivity[keyof ElementInteractivity]} * @returns {typeof ElementInteractivity[keyof typeof ElementInteractivity]}
*/ */
function element_interactivity(tag_name, attribute_map) { function element_interactivity(tag_name, attribute_map) {
if ( if (

@ -145,6 +145,7 @@ export function visit_component(node, context) {
if (slot_name !== 'default') comments = []; if (slot_name !== 'default') comments = [];
} }
/** @type {Set<string>} */
const component_slots = new Set(); const component_slots = new Set();
for (const slot_name in nodes) { for (const slot_name in nodes) {

@ -26,6 +26,7 @@ import { DebugTag } from './visitors/DebugTag.js';
import { EachBlock } from './visitors/EachBlock.js'; import { EachBlock } from './visitors/EachBlock.js';
import { ExportNamedDeclaration } from './visitors/ExportNamedDeclaration.js'; import { ExportNamedDeclaration } from './visitors/ExportNamedDeclaration.js';
import { ExpressionStatement } from './visitors/ExpressionStatement.js'; import { ExpressionStatement } from './visitors/ExpressionStatement.js';
import { ForOfStatement } from './visitors/ForOfStatement.js';
import { Fragment } from './visitors/Fragment.js'; import { Fragment } from './visitors/Fragment.js';
import { FunctionDeclaration } from './visitors/FunctionDeclaration.js'; import { FunctionDeclaration } from './visitors/FunctionDeclaration.js';
import { FunctionExpression } from './visitors/FunctionExpression.js'; import { FunctionExpression } from './visitors/FunctionExpression.js';
@ -103,6 +104,7 @@ const visitors = {
EachBlock, EachBlock,
ExportNamedDeclaration, ExportNamedDeclaration,
ExpressionStatement, ExpressionStatement,
ForOfStatement,
Fragment, Fragment,
FunctionDeclaration, FunctionDeclaration,
FunctionExpression, FunctionExpression,
@ -170,6 +172,7 @@ export function client_component(analysis, options) {
// these are set inside the `Fragment` visitor, and cannot be used until then // these are set inside the `Fragment` visitor, and cannot be used until then
init: /** @type {any} */ (null), init: /** @type {any} */ (null),
consts: /** @type {any} */ (null),
update: /** @type {any} */ (null), update: /** @type {any} */ (null),
after_update: /** @type {any} */ (null), after_update: /** @type {any} */ (null),
template: /** @type {any} */ (null), template: /** @type {any} */ (null),
@ -209,7 +212,8 @@ export function client_component(analysis, options) {
/** @type {ESTree.Statement[]} */ /** @type {ESTree.Statement[]} */
const store_setup = []; const store_setup = [];
/** @type {ESTree.Statement} */
let store_init = b.empty;
/** @type {ESTree.VariableDeclaration[]} */ /** @type {ESTree.VariableDeclaration[]} */
const legacy_reactive_declarations = []; const legacy_reactive_declarations = [];
@ -227,8 +231,9 @@ export function client_component(analysis, options) {
if (binding.kind === 'store_sub') { if (binding.kind === 'store_sub') {
if (store_setup.length === 0) { if (store_setup.length === 0) {
needs_store_cleanup = true; needs_store_cleanup = true;
store_setup.push( store_init = b.const(
b.const(b.array_pattern([b.id('$$stores'), b.id('$$cleanup')]), b.call('$.setup_stores')) b.array_pattern([b.id('$$stores'), b.id('$$cleanup')]),
b.call('$.setup_stores')
); );
} }
@ -354,16 +359,40 @@ export function client_component(analysis, options) {
if (dev) push_args.push(b.id(analysis.name)); if (dev) push_args.push(b.id(analysis.name));
let component_block = b.block([ let component_block = b.block([
store_init,
...store_setup, ...store_setup,
...legacy_reactive_declarations, ...legacy_reactive_declarations,
...group_binding_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))
]); ]);
if (analysis.instance.has_await) {
const body = b.block([
...state.instance_level_snippets,
.../** @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(
...state.instance_level_snippets,
.../** @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')))
);
}
const should_inject_context = const should_inject_context =
dev || dev ||
analysis.needs_context || analysis.needs_context ||
@ -378,32 +407,6 @@ export function client_component(analysis, options) {
analysis.uses_slots || analysis.uses_slots ||
analysis.slot_names.size > 0; analysis.slot_names.size > 0;
if (analysis.instance.has_await) {
const body = b.function_declaration(
b.id('$$body'),
should_inject_props ? [b.id('$$anchor'), b.id('$$props')] : [b.id('$$anchor')],
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'))),
b.stmt(b.call(body.id, b.id('node'), should_inject_props && b.id('$$props'))),
b.stmt(b.call('$.append', b.id('$$anchor'), b.id('fragment')))
]);
} else {
component_block.body.push(.../** @type {ESTree.Statement[]} */ (template.body));
}
// trick esrap into including comments // trick esrap into including comments
component_block.loc = instance.loc; component_block.loc = instance.loc;
@ -434,12 +437,6 @@ export function client_component(analysis, options) {
); );
} }
if (analysis.needs_mutation_validation) {
component_block.body.unshift(
b.var('$$ownership_validator', b.call('$.create_ownership_validator', b.id('$$props')))
);
}
// we want the cleanup function for the stores to run as the very last thing // we want the cleanup function for the stores to run as the very last thing
// so that it can effectively clean up the store subscription even after the user effects runs // so that it can effectively clean up the store subscription even after the user effects runs
if (should_inject_context) { if (should_inject_context) {
@ -610,8 +607,9 @@ export function client_component(analysis, options) {
); );
} }
if (analysis.custom_element) { const ce = options.customElementOptions ?? options.customElement;
const ce = analysis.custom_element;
if (ce) {
const ce_props = typeof ce === 'boolean' ? {} : ce.props || {}; const ce_props = typeof ce === 'boolean' ? {} : ce.props || {};
/** @type {ESTree.Property[]} */ /** @type {ESTree.Property[]} */

@ -6,7 +6,8 @@ import type {
Expression, Expression,
AssignmentExpression, AssignmentExpression,
UpdateExpression, UpdateExpression,
VariableDeclaration VariableDeclaration,
Declaration
} from 'estree'; } from 'estree';
import type { AST, Namespace, ValidatedCompileOptions } from '#compiler'; import type { AST, Namespace, ValidatedCompileOptions } from '#compiler';
import type { TransformState } from '../types.js'; import type { TransformState } from '../types.js';
@ -57,6 +58,8 @@ export interface ComponentClientTransformState extends ClientTransformState {
readonly update: Statement[]; readonly update: Statement[];
/** Stuff that happens after the render effect (control blocks, dynamic elements, bindings, actions, etc) */ /** Stuff that happens after the render effect (control blocks, dynamic elements, bindings, actions, etc) */
readonly after_update: Statement[]; readonly after_update: Statement[];
/** Transformed `{@const }` declarations */
readonly consts: Statement[];
/** Memoized expressions */ /** Memoized expressions */
readonly memoizer: Memoizer; readonly memoizer: Memoizer;
/** The HTML template string */ /** The HTML template string */

@ -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 { Binding } from '#compiler' */
/** @import { ClientTransformState, ComponentClientTransformState, ComponentContext } from './types.js' */ /** @import { ClientTransformState, ComponentClientTransformState, ComponentContext } from './types.js' */
/** @import { Analysis } 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 * Svelte legacy mode should use safe equals in most places, runes mode shouldn't
* @param {ComponentClientTransformState} state * @param {ComponentClientTransformState} state
* @param {Expression} arg * @param {Expression | BlockStatement} expression
* @param {boolean} [async]
*/ */
export function create_derived(state, arg) { export function create_derived(state, expression, async = false) {
return b.call(state.analysis.runes ? '$.derived' : '$.derived_safe_equal', arg); 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);
}
} }

@ -1,7 +1,7 @@
/** @import { BlockStatement, Pattern, Statement } from 'estree' */ /** @import { BlockStatement, Pattern, Statement } from 'estree' */
/** @import { AST } from '#compiler' */ /** @import { AST } from '#compiler' */
/** @import { ComponentClientTransformState, ComponentContext } from '../types' */ /** @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 * as b from '#compiler/builders';
import { create_derived } from '../utils.js'; import { create_derived } from '../utils.js';
import { get_value } from './shared/declarations.js'; import { get_value } from './shared/declarations.js';
@ -15,7 +15,10 @@ export function AwaitBlock(node, context) {
context.state.template.push_comment(); context.state.template.push_comment();
// Visit {#await <expression>} first to ensure that scopes are in the correct order // Visit {#await <expression>} 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 then_block;
let catch_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)))) 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) { for (const id of identifiers) {
context.state.transform[id.name] = { read: get_value }; context.state.transform[id.name] = { read: get_value };
declarations.push( 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)))
); );
} }

@ -85,7 +85,9 @@ export function CallExpression(node, context) {
['debug', 'dir', 'error', 'group', 'groupCollapsed', 'info', 'log', 'trace', 'warn'].includes( ['debug', 'dir', 'error', 'group', 'groupCollapsed', 'info', 'log', 'trace', 'warn'].includes(
node.callee.property.name 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( return b.call(
node.callee, node.callee,

@ -16,15 +16,26 @@ export function ConstTag(node, context) {
const declaration = node.declaration.declarations[0]; const declaration = node.declaration.declarations[0];
// TODO we can almost certainly share some code with $derived(...) // TODO we can almost certainly share some code with $derived(...)
if (declaration.id.type === 'Identifier') { if (declaration.id.type === 'Identifier') {
const init = build_expression(context, declaration.init, node.metadata.expression); const init = build_expression(
context.state.init.push(b.const(declaration.id, create_derived(context.state, b.thunk(init)))); { ...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.consts.push(b.const(declaration.id, expression));
context.state.transform[declaration.id.name] = { read: get_value }; context.state.transform[declaration.id.name] = { read: get_value };
// we need to eagerly evaluate the expression in order to hit any // we need to eagerly evaluate the expression in order to hit any
// 'Cannot access x before initialization' errors // 'Cannot access x before initialization' errors
if (dev) { 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 { } else {
const identifiers = extract_identifiers(declaration.id); const identifiers = extract_identifiers(declaration.id);
@ -38,7 +49,11 @@ export function ConstTag(node, context) {
delete transform[node.name]; 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` // TODO optimise the simple `{ x } = y` case — we can just return `y`
// instead of destructuring it only to return a new object // instead of destructuring it only to return a new object
@ -47,20 +62,24 @@ export function ConstTag(node, context) {
declaration.init, declaration.init,
node.metadata.expression 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))))
])
);
context.state.init.push(b.const(tmp, 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.consts.push(b.const(tmp, expression));
// we need to eagerly evaluate the expression in order to hit any // we need to eagerly evaluate the expression in order to hit any
// 'Cannot access x before initialization' errors // 'Cannot access x before initialization' errors
if (dev) { 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) { for (const node of identifiers) {

@ -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();
}

@ -48,6 +48,7 @@ export function Fragment(node, context) {
const is_single_child_not_needing_template = const is_single_child_not_needing_template =
trimmed.length === 1 && trimmed.length === 1 &&
(trimmed[0].type === 'SvelteFragment' || trimmed[0].type === 'TitleElement'); (trimmed[0].type === 'SvelteFragment' || trimmed[0].type === 'TitleElement');
const has_await = context.state.init !== null && (node.metadata.has_await || false);
const template_name = context.state.scope.root.unique('root'); // TODO infer name from parent const template_name = context.state.scope.root.unique('root'); // TODO infer name from parent
@ -61,6 +62,7 @@ export function Fragment(node, context) {
const state = { const state = {
...context.state, ...context.state,
init: [], init: [],
consts: [],
update: [], update: [],
after_update: [], after_update: [],
memoizer: new Memoizer(), memoizer: new Memoizer(),
@ -76,11 +78,6 @@ export function Fragment(node, context) {
context.visit(node, state); context.visit(node, state);
} }
if (is_text_first) {
// skip over inserted comment
body.push(b.stmt(b.call('$.next')));
}
if (is_single_element) { if (is_single_element) {
const element = /** @type {AST.RegularElement} */ (trimmed[0]); const element = /** @type {AST.RegularElement} */ (trimmed[0]);
@ -96,13 +93,13 @@ export function Fragment(node, context) {
const template = transform_template(state, namespace, flags); const template = transform_template(state, namespace, flags);
state.hoisted.push(b.var(template_name, template)); 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)); close = b.stmt(b.call('$.append', b.id('$$anchor'), id));
} else if (is_single_child_not_needing_template) { } else if (is_single_child_not_needing_template) {
context.visit(trimmed[0], state); context.visit(trimmed[0], state);
} else if (trimmed.length === 1 && trimmed[0].type === 'Text') { } else if (trimmed.length === 1 && trimmed[0].type === 'Text') {
const id = b.id(context.state.scope.generate('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)); close = b.stmt(b.call('$.append', b.id('$$anchor'), id));
} else if (trimmed.length > 0) { } else if (trimmed.length > 0) {
const id = b.id(context.state.scope.generate('fragment')); const id = b.id(context.state.scope.generate('fragment'));
@ -120,7 +117,7 @@ export function Fragment(node, context) {
state 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)); close = b.stmt(b.call('$.append', b.id('$$anchor'), id));
} else { } else {
if (is_standalone) { if (is_standalone) {
@ -140,12 +137,12 @@ export function Fragment(node, context) {
if (state.template.nodes.length === 1 && state.template.nodes[0].type === 'comment') { if (state.template.nodes.length === 1 && state.template.nodes[0].type === 'comment') {
// special case — we can use `$.comment` instead of creating a unique template // 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 { } else {
const template = transform_template(state, namespace, flags); const template = transform_template(state, namespace, flags);
state.hoisted.push(b.var(template_name, template)); 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)); 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); body.push(...state.init);
if (state.update.length > 0) { if (state.update.length > 0) {
@ -168,5 +176,9 @@ export function Fragment(node, context) {
body.push(close); 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);
}
} }

@ -46,9 +46,6 @@ export function LetDirective(node, context) {
read: (node) => b.call('$.get', node) read: (node) => b.call('$.get', node)
}; };
return b.const( return b.const(name, create_derived(context.state, b.member(b.id('$$slotProps'), node.name)));
name,
create_derived(context.state, b.thunk(b.member(b.id('$$slotProps'), node.name)))
);
} }
} }

@ -3,14 +3,14 @@
import * as b from '#compiler/builders'; import * as b from '#compiler/builders';
import { build_event, build_event_handler } from './shared/events.js'; import { build_event, build_event_handler } from './shared/events.js';
const modifiers = [ const modifiers = /** @type {const} */ ([
'stopPropagation', 'stopPropagation',
'stopImmediatePropagation', 'stopImmediatePropagation',
'preventDefault', 'preventDefault',
'self', 'self',
'trusted', 'trusted',
'once' 'once'
]; ]);
/** /**
* @param {AST.OnDirective} node * @param {AST.OnDirective} node

@ -14,6 +14,7 @@ export function SnippetBlock(node, context) {
// TODO hoist where possible // TODO hoist where possible
/** @type {(Identifier | AssignmentPattern)[]} */ /** @type {(Identifier | AssignmentPattern)[]} */
const args = [b.id('$$anchor')]; const args = [b.id('$$anchor')];
const has_await = node.body.metadata.has_await || false;
/** @type {BlockStatement} */ /** @type {BlockStatement} */
let body; let body;
@ -21,10 +22,6 @@ export function SnippetBlock(node, context) {
/** @type {Statement[]} */ /** @type {Statement[]} */
const declarations = []; const declarations = [];
if (dev) {
declarations.push(b.stmt(b.call('$.validate_snippet_args', b.spread(b.id('arguments')))));
}
const transform = { ...context.state.transform }; const transform = { ...context.state.transform };
const child_state = { ...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([ body = b.block([
dev ? b.stmt(b.call('$.validate_snippet_args', b.spread(b.id('arguments')))) : b.empty,
...declarations, ...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` // in dev we use a FunctionExpression (not arrow function) so we can use `arguments`
let snippet = dev let snippet = dev
? b.call('$.wrap_snippet', b.id(context.state.analysis.name), b.function(null, args, body)) ? b.call(
: b.arrow(args, body); '$.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); const declaration = b.const(node.expression, snippet);

@ -39,40 +39,60 @@ export function SvelteBoundary(node, context) {
/** @type {Statement[]} */ /** @type {Statement[]} */
const hoisted = []; const hoisted = [];
let has_const = false;
// const tags need to live inside the boundary, but might also be referenced in hoisted snippets. // 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 // 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) { for (const child of node.fragment.nodes) {
if (child.type === 'ConstTag') { 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) { for (const child of node.fragment.nodes) {
if (child.type === 'ConstTag') { if (child.type === 'ConstTag') {
if (context.state.options.experimental.async) {
nodes.push(child);
}
continue; continue;
} }
if (child.type === 'SnippetBlock') { if (child.type === 'SnippetBlock') {
/** @type {Statement[]} */ if (
const statements = []; context.state.options.experimental.async &&
has_const &&
context.visit(child, { ...context.state, init: statements }); !['failed', 'pending'].includes(child.expression.name)
) {
const snippet = /** @type {VariableDeclaration} */ (statements[0]); // we can't hoist snippets as they may reference const tags, so we just keep them in the fragment
nodes.push(child);
const snippet_fn = dev } else {
? // @ts-expect-error we know this shape is correct /** @type {Statement[]} */
snippet.declarations[0].init.arguments[1] const statements = [];
: snippet.declarations[0].init;
context.visit(child, { ...context.state, init: statements });
snippet_fn.body.body.unshift(
...const_tags.filter((node) => node.type === 'VariableDeclaration') const snippet = /** @type {VariableDeclaration} */ (statements[0]);
);
const snippet_fn = dev
hoisted.push(snippet); ? // @ts-expect-error we know this shape is correct
snippet.declarations[0].init.arguments[1]
if (['failed', 'pending'].includes(child.expression.name)) { : snippet.declarations[0].init;
props.properties.push(b.prop('init', child.expression, child.expression));
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; continue;
@ -83,7 +103,9 @@ export function SvelteBoundary(node, context) {
const block = /** @type {BlockStatement} */ (context.visit({ ...node.fragment, nodes })); 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( const boundary = b.stmt(
b.call('$.boundary', context.state.node, props, b.arrow([b.id('$$anchor')], block)) b.call('$.boundary', context.state.node, props, b.arrow([b.id('$$anchor')], block))

@ -171,11 +171,14 @@ export function VariableDeclaration(node, context) {
context.state.transform[id.name] = { read: get_value }; context.state.transform[id.name] = { read: get_value };
const expression = /** @type {Expression} */ (context.visit(b.thunk(value))); const expression = /** @type {Expression} */ (context.visit(b.thunk(value)));
const call = b.call('$.derived', expression); let call = b.call('$.derived', expression);
return b.declarator(
id, if (dev) {
dev ? b.call('$.tag', call, b.literal('[$state iterable]')) : call const label = `[$state ${declarator.id.type === 'ArrayPattern' ? 'iterable' : 'object'}]`;
); call = b.call('$.tag', call, b.literal(label));
}
return b.declarator(id, call);
}), }),
...paths.map((path) => { ...paths.map((path) => {
const value = /** @type {Expression} */ (context.visit(path.expression)); const value = /** @type {Expression} */ (context.visit(path.expression));
@ -228,19 +231,37 @@ export function VariableDeclaration(node, context) {
} }
} else { } else {
const init = /** @type {CallExpression} */ (declarator.init); const init = /** @type {CallExpression} */ (declarator.init);
let expression = /** @type {Expression} */ (
context.visit(value, {
...context.state,
in_derived: rune === '$derived'
})
);
let rhs = value; let rhs = value;
if (rune !== '$derived' || init.arguments[0].type !== 'Identifier') { if (rune !== '$derived' || init.arguments[0].type !== 'Identifier') {
const id = b.id(context.state.scope.generate('$$d')); const id = b.id(context.state.scope.generate('$$d'));
let call = b.call('$.derived', rune === '$derived' ? b.thunk(expression) : expression);
rhs = b.call('$.get', id); rhs = b.call('$.get', id);
let expression = /** @type {Expression} */ (context.visit(value)); if (is_async) {
if (rune === '$derived') expression = b.thunk(expression); const location = dev && !is_ignored(init, 'await_waterfall') && locate_node(init);
const call = b.call('$.derived', expression); call = b.call(
declarations.push( '$.async_derived',
b.declarator(id, dev ? b.call('$.tag', call, b.literal('[$derived iterable]')) : call) b.thunk(expression, true),
); location ? b.literal(location) : undefined
);
call = b.call(b.await(b.call('$.save', call)));
}
if (dev) {
const label = `[$derived ${declarator.id.type === 'ArrayPattern' ? 'iterable' : 'object'}]`;
call = b.call('$.tag', call, b.literal(label));
}
declarations.push(b.declarator(id, call));
} }
const { inserts, paths } = extract_paths(declarator.id, rhs); const { inserts, paths } = extract_paths(declarator.id, rhs);
@ -250,10 +271,14 @@ export function VariableDeclaration(node, context) {
context.state.transform[id.name] = { read: get_value }; context.state.transform[id.name] = { read: get_value };
const expression = /** @type {Expression} */ (context.visit(b.thunk(value))); const expression = /** @type {Expression} */ (context.visit(b.thunk(value)));
const call = b.call('$.derived', expression); let call = b.call('$.derived', expression);
declarations.push(
b.declarator(id, dev ? b.call('$.tag', call, b.literal('[$derived iterable]')) : call) if (dev) {
); const label = `[$derived ${declarator.id.type === 'ArrayPattern' ? 'iterable' : 'object'}]`;
call = b.call('$.tag', call, b.literal(label));
}
declarations.push(b.declarator(id, call));
} }
for (const path of paths) { for (const path of paths) {

@ -34,7 +34,7 @@ export class Memoizer {
} }
apply() { apply() {
return [...this.#async, ...this.#sync].map((memo, i) => { return [...this.#sync, ...this.#async].map((memo, i) => {
memo.id.name = `$${i}`; memo.id.name = `$${i}`;
return memo.id; return memo.id;
}); });

@ -65,13 +65,14 @@ export function visit_assignment_expression(node, context, build_assignment) {
statements.push(b.return(rhs)); statements.push(b.return(rhs));
} }
const iife = b.arrow([rhs], b.block(statements)); const async =
const iife_is_async =
is_expression_async(value) || is_expression_async(value) ||
assignments.some((assignment) => is_expression_async(assignment)); 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); const sequence = b.sequence(assignments);

@ -23,6 +23,15 @@ export function is_element_node(node) {
return element_nodes.includes(node.type); 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 * @param {AST.RegularElement | AST.SvelteElement} node
* @returns {boolean} * @returns {boolean}

@ -22,7 +22,7 @@ const NUMBER = Symbol('number');
const STRING = Symbol('string'); const STRING = Symbol('string');
const FUNCTION = Symbol('string'); const FUNCTION = Symbol('string');
/** @type {Record<string, [type: NUMBER | STRING | UNKNOWN, fn?: Function]>} */ /** @type {Record<string, [type: typeof NUMBER | typeof STRING | typeof UNKNOWN, fn?: Function]>} */
const globals = { const globals = {
BigInt: [NUMBER], BigInt: [NUMBER],
'Math.min': [NUMBER, Math.min], 'Math.min': [NUMBER, Math.min],
@ -122,7 +122,7 @@ export class Binding {
/** /**
* Additional metadata, varies per binding type * Additional metadata, varies per binding type
* @type {null | { inside_rest?: boolean }} * @type {null | { inside_rest?: boolean; is_template_declaration?: boolean }}
*/ */
metadata = null; metadata = null;
@ -180,6 +180,13 @@ class Evaluation {
*/ */
is_known = true; 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 * True if the value is known to not be null/undefined
* @readonly * @readonly
@ -540,6 +547,10 @@ class Evaluation {
if (value == null || value === UNKNOWN) { if (value == null || value === UNKNOWN) {
this.is_defined = false; this.is_defined = false;
} }
if (value === UNKNOWN) {
this.has_unknown = true;
}
} }
if (this.values.size > 1 || typeof this.value === 'symbol') { 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, node.kind,
declarator.init declarator.init
); );
binding.metadata = { is_template_declaration: true };
bindings.push(binding); bindings.push(binding);
} }
} }

@ -95,7 +95,6 @@ export interface ComponentAnalysis extends Analysis {
}; };
/** @deprecated use `source` from `state.js` instead */ /** @deprecated use `source` from `state.js` instead */
source: string; source: string;
undefined_exports: Map<string, Node>;
/** /**
* Every render tag/component, and whether it could be definitively resolved or not * Every render tag/component, and whether it could be definitively resolved or not
*/ */

@ -1,6 +1,6 @@
/** @import { Processed, Preprocessor, MarkupPreprocessor, PreprocessorGroup } from './public.js' */ /** @import { Processed, Preprocessor, MarkupPreprocessor, PreprocessorGroup } from './public.js' */
/** @import { SourceUpdate, Source } from './private.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 { getLocator } from 'locate-character';
import { import {
MappedCode, MappedCode,
@ -25,7 +25,7 @@ class PreprocessResult {
// sourcemap_list is sorted in reverse order from last map (index 0) to first map (index -1) // 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 // 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 [] * @default []

@ -1,4 +1,4 @@
import { DecodedSourceMap } from '@ampproject/remapping'; import { DecodedSourceMap } from '@jridgewell/remapping';
import { Location } from 'locate-character'; import { Location } from 'locate-character';
import { MappedCode } from '../utils/mapped_code.js'; import { MappedCode } from '../utils/mapped_code.js';

@ -87,7 +87,7 @@ export function pop_ignore() {
/** /**
* @param {AST.SvelteNode | NodeLike} node * @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 * @returns
*/ */
export function is_ignored(node, code) { export function is_ignored(node, code) {

@ -56,6 +56,7 @@ export namespace AST {
* Whether or not we need to traverse into the fragment during mount/hydrate * Whether or not we need to traverse into the fragment during mount/hydrate
*/ */
dynamic: boolean; dynamic: boolean;
has_await: boolean;
}; };
} }
@ -247,7 +248,17 @@ export namespace AST {
name: string; name: string;
/** The 'y' in `on:x={y}` */ /** The 'y' in `on:x={y}` */
expression: null | Expression; expression: null | Expression;
modifiers: string[]; // TODO specify modifiers: Array<
| 'capture'
| 'nonpassive'
| 'once'
| 'passive'
| 'preventDefault'
| 'self'
| 'stopImmediatePropagation'
| 'stopPropagation'
| 'trusted'
>;
/** @internal */ /** @internal */
metadata: { metadata: {
expression: ExpressionMetadata; expression: ExpressionMetadata;

@ -56,15 +56,6 @@ export function assignment(operator, left, right) {
return { type: 'AssignmentExpression', 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 * @param {ESTree.Expression} argument
* @returns {ESTree.AwaitExpression} * @returns {ESTree.AwaitExpression}
@ -214,6 +205,23 @@ export function export_default(declaration) {
return { type: 'ExportDefaultDeclaration', 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.Identifier} id
* @param {ESTree.Pattern[]} params * @param {ESTree.Pattern[]} params
@ -580,14 +588,14 @@ export function method(kind, key, params, body, computed = false, is_static = fa
* @param {ESTree.BlockStatement} body * @param {ESTree.BlockStatement} body
* @returns {ESTree.FunctionExpression} * @returns {ESTree.FunctionExpression}
*/ */
function function_builder(id, params, body) { function function_builder(id, params, body, async = false) {
return { return {
type: 'FunctionExpression', type: 'FunctionExpression',
id, id,
params, params,
body, body,
generator: false, generator: false,
async: false, async,
metadata: /** @type {any} */ (null) // should not be used by codegen metadata: /** @type {any} */ (null) // should not be used by codegen
}; };
} }

@ -2,8 +2,8 @@
/** @import { Processed } from '../preprocess/public.js' */ /** @import { Processed } from '../preprocess/public.js' */
/** @import { SourceMap } from 'magic-string' */ /** @import { SourceMap } from 'magic-string' */
/** @import { Source } from '../preprocess/private.js' */ /** @import { Source } from '../preprocess/private.js' */
/** @import { DecodedSourceMap, SourceMapSegment, RawSourceMap } from '@ampproject/remapping' */ /** @import { DecodedSourceMap, SourceMapSegment, RawSourceMap } from '@jridgewell/remapping' */
import remapping from '@ampproject/remapping'; import remapping from '@jridgewell/remapping';
import { push_array } from './push_array.js'; import { push_array } from './push_array.js';
/** /**

@ -8,7 +8,7 @@ import * as w from './warnings.js';
* @typedef {(input: Input, keypath: string) => Required<Output>} Validator * @typedef {(input: Input, keypath: string) => Required<Output>} Validator
*/ */
const common = { const common_options = {
filename: string('(unknown)'), filename: string('(unknown)'),
// default to process.cwd() where it exists to replicate svelte4 behavior (and make Deno work with this as well) // 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 = const component_options = {
/** @type {Validator<ModuleCompileOptions, ValidatedModuleCompileOptions>} */ ( accessors: deprecate(w.options_deprecated_accessors, boolean(false)),
object({
...common
})
);
export const validate_component_options = css: validator('external', (input) => {
/** @type {Validator<CompileOptions, ValidatedCompileOptions>} */ ( if (input === true || input === false) {
object({ throw_error(
...common, '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) => { return 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.'
);
}
if (input !== 'external' && input !== 'injected') { cssHash: fun(({ css, hash }) => {
throw_error(`css should be either "external" (default, recommended) or "injected"`); 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 }) => { legacy: removed(
return `svelte-${hash(css)}`; '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 namespace: list(['html', 'mathml', 'svg']),
cssOutputFilename: string(undefined),
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( fragments: list(['html', 'tree']),
'The legacy option has been removed. If you are using this because of legacy.componentApi, use compatibility.componentApi instead'
),
compatibility: object({ preserveWhitespace: boolean(false),
componentApi: list([4, 5], 5)
}),
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 `<svelte:options customElement="tag-name" />` 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) => { export const validate_module_options =
// Source maps can take on a variety of values, including string, JSON, map objects from magic-string and source-map, /** @type {Validator<ModuleCompileOptions, ValidatedModuleCompileOptions>} */ (
// so there's no good way to check type validity here object({
return input; ...common_options,
}), ...Object.fromEntries(Object.keys(component_options).map((key) => [key, () => {}]))
})
);
enableSourcemap: warn_removed(w.options_removed_enable_sourcemap), export const validate_component_options =
hydratable: warn_removed(w.options_removed_hydratable), /** @type {Validator<CompileOptions, ValidatedCompileOptions>} */ (
format: removed( object({
'The format option has been removed in Svelte 4, the compiler only outputs ESM now. Remove "format" from your compiler options. ' + ...common_options,
'If you did not set this yourself, bump the version of your bundler plugin (vite-plugin-svelte/rollup-plugin-svelte/svelte-loader)' ...component_options
),
tag: removed(
'The tag option has been removed in Svelte 5. Use `<svelte:options customElement="tag-name" />` 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'
)
}) })
); );

@ -160,10 +160,14 @@ export function createEventDispatcher() {
e.lifecycle_outside_component('createEventDispatcher'); e.lifecycle_outside_component('createEventDispatcher');
} }
/**
* @param [detail]
* @param [options]
*/
return (type, detail, options) => { return (type, detail, options) => {
const events = /** @type {Record<string, Function | Function[]>} */ ( const events = /** @type {Record<string, Function | Function[]>} */ (
active_component_context.s.$$events active_component_context.s.$$events
)?.[/** @type {any} */ (type)]; )?.[/** @type {string} */ (type)];
if (events) { if (events) {
const callbacks = is_array(events) ? events.slice() : [events]; const callbacks = is_array(events) ? events.slice() : [events];

@ -63,6 +63,13 @@ export function log_effect_tree(effect, depth = 0) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.log(callsite); console.log(callsite);
} else {
// eslint-disable-next-line no-console
console.groupCollapsed(`%cfn`, `font-weight: normal`);
// eslint-disable-next-line no-console
console.log(effect.fn);
// eslint-disable-next-line no-console
console.groupEnd();
} }
if (effect.deps !== null) { if (effect.deps !== null) {

@ -64,7 +64,7 @@ export function hmr(original, get_source) {
// @ts-expect-error // @ts-expect-error
wrapper[FILENAME] = original[FILENAME]; wrapper[FILENAME] = original[FILENAME];
// @ts-expect-error // @ts-ignore
wrapper[HMR] = { wrapper[HMR] = {
// When we accept an update, we set the original source to the new component // When we accept an update, we set the original source to the new component
original, original,

@ -26,7 +26,7 @@ export function inspect(get_value, inspector = console.log) {
return; return;
} }
var snap = snapshot(value, true); var snap = snapshot(value, true, true);
untrack(() => { untrack(() => {
inspector(initial ? 'init' : 'update', ...snap); inspector(initial ? 'init' : 'update', ...snap);
}); });

@ -4,7 +4,7 @@ import { snapshot } from '../../shared/clone.js';
import { define_property } from '../../shared/utils.js'; import { define_property } from '../../shared/utils.js';
import { DERIVED, ASYNC, PROXY_PATH_SYMBOL, STATE_SYMBOL } from '#client/constants'; import { DERIVED, ASYNC, PROXY_PATH_SYMBOL, STATE_SYMBOL } from '#client/constants';
import { effect_tracking } from '../reactivity/effects.js'; import { effect_tracking } from '../reactivity/effects.js';
import { active_reaction, captured_signals, set_captured_signals, untrack } from '../runtime.js'; import { active_reaction, untrack } from '../runtime.js';
/** /**
* @typedef {{ * @typedef {{
@ -26,7 +26,7 @@ function log_entry(signal, entry) {
return; return;
} }
const type = (signal.f & (DERIVED | ASYNC)) !== 0 ? '$derived' : '$state'; const type = get_type(signal);
const current_reaction = /** @type {Reaction} */ (active_reaction); const current_reaction = /** @type {Reaction} */ (active_reaction);
const dirty = signal.wv > current_reaction.wv || current_reaction.wv === 0; const dirty = signal.wv > current_reaction.wv || current_reaction.wv === 0;
const style = dirty const style = dirty
@ -73,6 +73,15 @@ function log_entry(signal, entry) {
console.groupEnd(); console.groupEnd();
} }
/**
* @param {Value} signal
* @returns {'$state' | '$derived' | 'store'}
*/
function get_type(signal) {
if ((signal.f & (DERIVED | ASYNC)) !== 0) return '$derived';
return signal.label?.startsWith('$') ? 'store' : '$state';
}
/** /**
* @template T * @template T
* @param {() => string} label * @param {() => string} label

@ -28,6 +28,8 @@ const PENDING = 0;
const THEN = 1; const THEN = 1;
const CATCH = 2; const CATCH = 2;
/** @typedef {typeof PENDING | typeof THEN | typeof CATCH} AwaitState */
/** /**
* @template V * @template V
* @param {TemplateNode} node * @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); : mutable_source(/** @type {V} */ (undefined), false, false);
var error_source = runes ? source(undefined) : mutable_source(undefined, false, false); var error_source = runes ? source(undefined) : mutable_source(undefined, false, false);
var resolved = false; var resolved = false;
/** /**
* @param {PENDING | THEN | CATCH} state * @param {AwaitState} state
* @param {boolean} restore * @param {boolean} restore
*/ */
function update(state, restore) { function update(state, restore) {

@ -148,7 +148,7 @@ export class Boundary {
// need to use hydration boundary comments to report whether // need to use hydration boundary comments to report whether
// the pending or main block was rendered for a given // the pending or main block was rendered for a given
// boundary, and hydrate accordingly // boundary, and hydrate accordingly
queueMicrotask(() => { Batch.enqueue(() => {
this.#main_effect = this.#run(() => { this.#main_effect = this.#run(() => {
Batch.ensure(); Batch.ensure();
return branch(() => this.#children(this.#anchor)); return branch(() => this.#children(this.#anchor));

@ -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 // store a reference to the effect so that we can update the start/end nodes in reconciliation
each_effect ??= /** @type {Effect} */ (active_effect); each_effect ??= /** @type {Effect} */ (active_effect);
array = get(each_array); array = /** @type {V[]} */ (get(each_array));
var length = array.length; var length = array.length;
if (was_empty && length === 0) { if (was_empty && length === 0) {

@ -36,7 +36,7 @@ export function if_block(node, fn, elseif = false) {
/** @type {Effect | null} */ /** @type {Effect | null} */
var alternate_effect = null; var alternate_effect = null;
/** @type {UNINITIALIZED | boolean | null} */ /** @type {typeof UNINITIALIZED | boolean | null} */
var condition = UNINITIALIZED; var condition = UNINITIALIZED;
var flags = elseif ? EFFECT_TRANSPARENT : 0; var flags = elseif ? EFFECT_TRANSPARENT : 0;

@ -62,8 +62,10 @@ export function component(node, get_component, render_fn) {
if (defer) { if (defer) {
offscreen_fragment = document.createDocumentFragment(); offscreen_fragment = document.createDocumentFragment();
offscreen_fragment.append((target = create_text())); offscreen_fragment.append((target = create_text()));
if (effect) {
/** @type {Batch} */ (current_batch).skipped_effects.add(effect);
}
} }
pending_effect = branch(() => render_fn(target, component)); pending_effect = branch(() => render_fn(target, component));
} }

@ -1,6 +1,6 @@
import { DEV } from 'esm-env'; import { DEV } from 'esm-env';
import { queue_micro_task } from './task.js';
import { register_style } from '../dev/css.js'; import { register_style } from '../dev/css.js';
import { effect } from '../reactivity/effects.js';
/** /**
* @param {Node} anchor * @param {Node} anchor
@ -8,7 +8,7 @@ import { register_style } from '../dev/css.js';
*/ */
export function append_styles(anchor, css) { export function append_styles(anchor, css) {
// Use `queue_micro_task` to ensure `anchor` is in the DOM, otherwise getRootNode() will yield wrong results // 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 root = anchor.getRootNode();
var target = /** @type {ShadowRoot} */ (root).host var target = /** @type {ShadowRoot} */ (root).host

@ -19,7 +19,7 @@ import { attach } from './attachments.js';
import { clsx } from '../../../shared/attributes.js'; import { clsx } from '../../../shared/attributes.js';
import { set_class } from './class.js'; import { set_class } from './class.js';
import { set_style } from './style.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 { block, branch, destroy_effect, effect } from '../../reactivity/effects.js';
import { init_select, select_option } from './bindings/select.js'; import { init_select, select_option } from './bindings/select.js';
import { flatten } from '../../reactivity/async.js'; import { flatten } from '../../reactivity/async.js';
@ -238,10 +238,10 @@ export function set_custom_element_data(node, prop, value) {
// Don't compute setters for custom elements while they aren't registered yet, // Don't compute setters for custom elements while they aren't registered yet,
// because during their upgrade/instantiation they might add more setters. // because during their upgrade/instantiation they might add more setters.
// Instead, fall back to a simple "an object, then set as property" heuristic. // Instead, fall back to a simple "an object, then set as property" heuristic.
(setters_cache.has(node.nodeName) || (setters_cache.has(node.getAttribute('is') || node.nodeName) ||
// customElements may not be available in browser extension contexts // customElements may not be available in browser extension contexts
!customElements || !customElements ||
customElements.get(node.tagName.toLowerCase()) customElements.get(node.getAttribute('is') || node.tagName.toLowerCase())
? get_setters(node).includes(prop) ? get_setters(node).includes(prop)
: value && typeof value === 'object') : value && typeof value === 'object')
) { ) {
@ -446,6 +446,8 @@ export function set_attributes(element, prev, next, css_hash, skip_warning = fal
) { ) {
// @ts-ignore // @ts-ignore
element[name] = value; element[name] = value;
// remove it from attributes's cache
if (name in attributes) attributes[name] = UNINITIALIZED;
} else if (typeof value !== 'function') { } else if (typeof value !== 'function') {
set_attribute(element, name, value, skip_warning); set_attribute(element, name, value, skip_warning);
} }
@ -544,9 +546,10 @@ var setters_cache = new Map();
/** @param {Element} element */ /** @param {Element} element */
function get_setters(element) { function get_setters(element) {
var setters = setters_cache.get(element.nodeName); var cache_key = element.getAttribute('is') || element.nodeName;
var setters = setters_cache.get(cache_key);
if (setters) return setters; if (setters) return setters;
setters_cache.set(element.nodeName, (setters = [])); setters_cache.set(cache_key, (setters = []));
var descriptors; var descriptors;
var proto = element; // In the case of custom elements there might be setters on the instance var proto = element; // In the case of custom elements there might be setters on the instance

@ -6,9 +6,9 @@ import * as e from '../../../errors.js';
import { is } from '../../../proxy.js'; import { is } from '../../../proxy.js';
import { queue_micro_task } from '../../task.js'; import { queue_micro_task } from '../../task.js';
import { hydrating } from '../../hydration.js'; import { hydrating } from '../../hydration.js';
import { untrack } from '../../../runtime.js'; import { tick, untrack } from '../../../runtime.js';
import { is_runes } from '../../../context.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 * @param {HTMLInputElement} input
@ -17,11 +17,9 @@ import { current_batch } from '../../../reactivity/batch.js';
* @returns {void} * @returns {void}
*/ */
export function bind_value(input, get, set = get) { export function bind_value(input, get, set = get) {
var runes = is_runes();
var batches = new WeakSet(); var batches = new WeakSet();
listen_to_event_and_reset_event(input, 'input', (is_reset) => { listen_to_event_and_reset_event(input, 'input', async (is_reset) => {
if (DEV && input.type === 'checkbox') { if (DEV && input.type === 'checkbox') {
// TODO should this happen in prod too? // TODO should this happen in prod too?
e.bind_invalid_checkbox_value(); e.bind_invalid_checkbox_value();
@ -36,9 +34,13 @@ export function bind_value(input, get, set = get) {
batches.add(current_batch); batches.add(current_batch);
} }
// In runes mode, respect any validation in accessors (doesn't apply in legacy mode, // Because `{#each ...}` blocks work by updating sources inside the flush,
// because we use mutable state which ensures the render effect always runs) // we need to wait a tick before checking to see if we should forcibly
if (runes && value !== (value = get())) { // update the input and reset the selection state
await tick();
// Respect any validation in accessors
if (value !== (value = get())) {
var start = input.selectionStart; var start = input.selectionStart;
var end = input.selectionEnd; var end = input.selectionEnd;
@ -76,13 +78,18 @@ export function bind_value(input, get, set = get) {
var value = 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, // 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: // an update is deferred because of async work depending on the input:
// //
// <input bind:value={query}> // <input bind:value={query}>
// <p>{await find(query)}</p> // <p>{await find(query)}</p>
return; if (batches.has(batch)) {
return;
}
} }
if (is_numberlike_input(input) && value === to_number(input.value)) { if (is_numberlike_input(input) && value === to_number(input.value)) {
@ -240,6 +247,7 @@ export function bind_checked(input, get, set = get) {
* @returns {V[]} * @returns {V[]}
*/ */
function get_binding_group_value(group, __value, checked) { function get_binding_group_value(group, __value, checked) {
/** @type {Set<V>} */
var value = new Set(); var value = new Set();
for (var i = 0; i < group.length; i += 1) { for (var i = 0; i < group.length; i += 1) {

@ -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} * @this {EventTarget}
* @param {Event} event * @param {Event} event
@ -153,14 +160,19 @@ export function handle_event_propagation(event) {
var path = event.composedPath?.() || []; var path = event.composedPath?.() || [];
var current_target = /** @type {null | Element} */ (path[0] || event.target); var current_target = /** @type {null | Element} */ (path[0] || event.target);
last_propagated_event = event;
// composedPath contains list of nodes the event has propagated through. // composedPath contains list of nodes the event has propagated through.
// We check __root to skip all nodes below it in case this is a // 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 // 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. // mounted apps. In this case we don't want to trigger events multiple times.
var path_idx = 0; 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 // @ts-expect-error is added below
var handled_at = event.__root; var handled_at = last_propagated_event === event && event.__root;
if (handled_at) { if (handled_at) {
var at_idx = path.indexOf(handled_at); var at_idx = path.indexOf(handled_at);

@ -6,6 +6,7 @@ import { get_descriptor, is_extensible } from '../../shared/utils.js';
import { active_effect } from '../runtime.js'; import { active_effect } from '../runtime.js';
import { async_mode_flag } from '../../flags/index.js'; import { async_mode_flag } from '../../flags/index.js';
import { TEXT_NODE, EFFECT_RAN } from '#client/constants'; 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 // export these for reference in the compiled code, making global name deduplication unnecessary
/** @type {Window} */ /** @type {Window} */
@ -214,6 +215,7 @@ export function clear_text_content(node) {
*/ */
export function should_defer_append() { export function should_defer_append() {
if (!async_mode_flag) return false; if (!async_mode_flag) return false;
if (eager_block_effects !== null) return false;
var flags = /** @type {Effect} */ (active_effect).f; var flags = /** @type {Effect} */ (active_effect).f;
return (flags & EFFECT_RAN) !== 0; return (flags & EFFECT_RAN) !== 0;

@ -156,7 +156,7 @@ export function from_mathml(content, flags) {
/** /**
* @param {TemplateStructure[]} structure * @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) { function fragment_from_tree(structure, ns) {
var fragment = create_fragment(); var fragment = create_fragment();

@ -98,7 +98,12 @@ export {
props_id, props_id,
with_script with_script
} from './dom/template.js'; } 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 { flushSync as flush, suspend } from './reactivity/batch.js';
export { export {
async_derived, async_derived,
@ -139,16 +144,17 @@ export {
mark_store_binding mark_store_binding
} from './reactivity/store.js'; } from './reactivity/store.js';
export { boundary, pending } from './dom/blocks/boundary.js'; export { boundary, pending } from './dom/blocks/boundary.js';
export { invalidate_inner_signals } from './legacy.js';
export { set_text } from './render.js'; export { set_text } from './render.js';
export { export {
get, get,
safe_get, safe_get,
invalidate_inner_signals,
tick, tick,
untrack, untrack,
exclude_from_object, exclude_from_object,
deep_read, deep_read,
deep_read_state deep_read_state,
active_effect
} from './runtime.js'; } from './runtime.js';
export { validate_binding, validate_each_keys } from './validate.js'; export { validate_binding, validate_each_keys } from './validate.js';
export { raf } from './timing.js'; export { raf } from './timing.js';
@ -173,3 +179,4 @@ export {
} from '../shared/validate.js'; } from '../shared/validate.js';
export { strict_equals, equals } from './dev/equality.js'; export { strict_equals, equals } from './dev/equality.js';
export { log_if_contains_state } from './dev/console-log.js'; export { log_if_contains_state } from './dev/console-log.js';
export { invoke_error_boundary } from './error-handling.js';

@ -0,0 +1,46 @@
/** @import { Value } from '#client' */
import { internal_set } from './reactivity/sources.js';
import { untrack } from './runtime.js';
/**
* @type {Set<Value> | null}
* @deprecated
*/
export let captured_signals = null;
/**
* Capture an array of all the signals that are read when `fn` is called
* @template T
* @param {() => T} fn
*/
function capture_signals(fn) {
var previous_captured_signals = captured_signals;
try {
captured_signals = new Set();
untrack(fn);
if (previous_captured_signals !== null) {
for (var signal of captured_signals) {
previous_captured_signals.add(signal);
}
}
return captured_signals;
} finally {
captured_signals = previous_captured_signals;
}
}
/**
* Invokes a function and captures all signals that are read during the invocation,
* then invalidates them.
* @param {() => any} fn
* @deprecated
*/
export function invalidate_inner_signals(fn) {
for (var signal of capture_signals(fn)) {
internal_set(signal, signal.v);
}
}

@ -93,9 +93,11 @@ export function proxy(value) {
/** Used in dev for $inspect.trace() */ /** Used in dev for $inspect.trace() */
var path = ''; var path = '';
let updating = false;
/** @param {string} new_path */ /** @param {string} new_path */
function update_path(new_path) { function update_path(new_path) {
if (updating) return;
updating = true;
path = new_path; path = new_path;
tag(version, `${path} version`); tag(version, `${path} version`);
@ -104,6 +106,7 @@ export function proxy(value) {
for (const [prop, source] of sources) { for (const [prop, source] of sources) {
tag(source, get_label(path, prop)); tag(source, get_label(path, prop));
} }
updating = false;
} }
return new Proxy(/** @type {any} */ (value), { return new Proxy(/** @type {any} */ (value), {
@ -284,13 +287,13 @@ export function proxy(value) {
if (s === undefined) { if (s === undefined) {
if (!has || get_descriptor(target, prop)?.writable) { if (!has || get_descriptor(target, prop)?.writable) {
s = with_parent(() => source(undefined, stack)); s = with_parent(() => source(undefined, stack));
set(s, proxy(value));
sources.set(prop, s);
if (DEV) { if (DEV) {
tag(s, get_label(path, prop)); tag(s, get_label(path, prop));
} }
set(s, proxy(value));
sources.set(prop, s);
} }
} else { } else {
has = s.v !== UNINITIALIZED; has = s.v !== UNINITIALIZED;

@ -11,7 +11,7 @@ import {
set_active_effect, set_active_effect,
set_active_reaction set_active_reaction
} from '../runtime.js'; } from '../runtime.js';
import { current_batch } from './batch.js'; import { current_batch, suspend } from './batch.js';
import { import {
async_derived, async_derived,
current_async_effect, current_async_effect,
@ -19,6 +19,7 @@ import {
derived_safe_equal, derived_safe_equal,
set_from_async_derived set_from_async_derived
} from './deriveds.js'; } from './deriveds.js';
import { aborted } from './effects.js';
/** /**
* *
@ -72,11 +73,13 @@ function capture() {
var previous_effect = active_effect; var previous_effect = active_effect;
var previous_reaction = active_reaction; var previous_reaction = active_reaction;
var previous_component_context = component_context; var previous_component_context = component_context;
var previous_batch = current_batch;
return function restore() { return function restore() {
set_active_effect(previous_effect); set_active_effect(previous_effect);
set_active_reaction(previous_reaction); set_active_reaction(previous_reaction);
set_component_context(previous_component_context); set_component_context(previous_component_context);
previous_batch?.activate();
if (DEV) { if (DEV) {
set_from_async_derived(null); set_from_async_derived(null);
@ -119,9 +122,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<T> | AsyncIterable<T>} iterable
* @returns {AsyncGenerator<T, TReturn | undefined>}
*/
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<T, TReturn>} */
// @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() { export function unset_context() {
set_active_effect(null); set_active_effect(null);
set_active_reaction(null); set_active_reaction(null);
set_component_context(null); set_component_context(null);
if (DEV) set_from_async_derived(null); if (DEV) set_from_async_derived(null);
} }
/**
* @param {() => Promise<void>} fn
*/
export async function async_body(fn) {
var unsuspend = suspend();
var active = /** @type {Effect} */ (active_effect);
try {
await fn();
} catch (error) {
if (!aborted(active)) {
invoke_error_boundary(error, active);
}
} finally {
unsuspend();
}
}

@ -10,7 +10,8 @@ import {
INERT, INERT,
RENDER_EFFECT, RENDER_EFFECT,
ROOT_EFFECT, ROOT_EFFECT,
USER_EFFECT USER_EFFECT,
MAYBE_DIRTY
} from '#client/constants'; } from '#client/constants';
import { async_mode_flag } from '../../flags/index.js'; import { async_mode_flag } from '../../flags/index.js';
import { deferred, define_property } from '../../shared/utils.js'; import { deferred, define_property } from '../../shared/utils.js';
@ -21,8 +22,7 @@ import {
is_updating_effect, is_updating_effect,
set_is_updating_effect, set_is_updating_effect,
set_signal_status, set_signal_status,
update_effect, update_effect
write_version
} from '../runtime.js'; } from '../runtime.js';
import * as e from '../errors.js'; import * as e from '../errors.js';
import { flush_tasks } from '../dom/task.js'; import { flush_tasks } from '../dom/task.js';
@ -38,6 +38,13 @@ const batches = new Set();
/** @type {Batch | null} */ /** @type {Batch | null} */
export let current_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 * When time travelling, we re-evaluate deriveds based on the temporary
* values of their dependencies rather than their actual values, and cache * values of their dependencies rather than their actual values, and cache
@ -49,6 +56,19 @@ export let batch_deriveds = null;
/** @type {Set<() => void>} */ /** @type {Set<() => void>} */
export let effect_pending_updates = new Set(); export let effect_pending_updates = new Set();
/** @type {Array<() => void>} */
let tasks = [];
function dequeue() {
const task = /** @type {() => void} */ (tasks.shift());
if (tasks.length > 0) {
queueMicrotask(dequeue);
}
task();
}
/** @type {Effect[]} */ /** @type {Effect[]} */
let queued_root_effects = []; let queued_root_effects = [];
@ -56,6 +76,7 @@ let queued_root_effects = [];
let last_scheduled_effect = null; let last_scheduled_effect = null;
let is_flushing = false; let is_flushing = false;
let is_flushing_sync = false;
export class Batch { export class Batch {
/** /**
@ -63,7 +84,7 @@ export class Batch {
* They keys of this map are identical to `this.#previous` * They keys of this map are identical to `this.#previous`
* @type {Map<Source, any>} * @type {Map<Source, any>}
*/ */
#current = new Map(); current = new Map();
/** /**
* The values of any sources that are updated in this batch _before_ those updates took place. * The values of any sources that are updated in this batch _before_ those updates took place.
@ -132,6 +153,18 @@ export class Batch {
*/ */
#block_effects = []; #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 * A set of branches that still exist, but will be destroyed when this batch
* is committed we skip over these during `process` * is committed we skip over these during `process`
@ -143,20 +176,22 @@ export class Batch {
* *
* @param {Effect[]} root_effects * @param {Effect[]} root_effects
*/ */
#process(root_effects) { process(root_effects) {
queued_root_effects = []; queued_root_effects = [];
previous_batch = null;
/** @type {Map<Source, { v: unknown, wv: number }> | null} */ /** @type {Map<Source, { v: unknown, wv: number }> | null} */
var current_values = null; var current_values = null;
// if there are multiple batches, we are 'time travelling' — // if there are multiple batches, we are 'time travelling' —
// we need to undo the changes belonging to any batch // we need to undo the changes belonging to any batch
// other than the current one // other than the current one
if (batches.size > 1) { if (async_mode_flag && batches.size > 1) {
current_values = new Map(); current_values = new Map();
batch_deriveds = 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 }); current_values.set(source, { v: source.v, wv: source.wv });
source.v = current; source.v = current;
} }
@ -180,6 +215,8 @@ export class Batch {
// if we didn't start any new async work, and no async work // if we didn't start any new async work, and no async work
// is outstanding from a previous flush, commit // is outstanding from a previous flush, commit
if (this.#async_effects.length === 0 && this.#pending === 0) { if (this.#async_effects.length === 0 && this.#pending === 0) {
this.#commit();
var render_effects = this.#render_effects; var render_effects = this.#render_effects;
var effects = this.#effects; var effects = this.#effects;
@ -187,17 +224,28 @@ export class Batch {
this.#effects = []; this.#effects = [];
this.#block_effects = []; this.#block_effects = [];
this.#commit(); // 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(render_effects);
flush_queued_effects(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(); this.#deferred?.resolve();
} else { } else {
// otherwise mark effects clean so they get scheduled on the next run this.#defer_effects(this.#render_effects);
for (const e of this.#render_effects) set_signal_status(e, CLEAN); this.#defer_effects(this.#effects);
for (const e of this.#effects) set_signal_status(e, CLEAN); this.#defer_effects(this.#block_effects);
for (const e of this.#block_effects) set_signal_status(e, CLEAN);
} }
if (current_values) { if (current_values) {
@ -248,11 +296,11 @@ export class Batch {
this.#effects.push(effect); this.#effects.push(effect);
} else if (async_mode_flag && (flags & RENDER_EFFECT) !== 0) { } else if (async_mode_flag && (flags & RENDER_EFFECT) !== 0) {
this.#render_effects.push(effect); this.#render_effects.push(effect);
} else if (is_dirty(effect)) { } else if ((flags & CLEAN) === 0) {
if ((flags & ASYNC) !== 0) { if ((flags & ASYNC) !== 0) {
var effects = effect.b?.pending ? this.#boundary_async_effects : this.#async_effects; var effects = effect.b?.pending ? this.#boundary_async_effects : this.#async_effects;
effects.push(effect); effects.push(effect);
} else { } else if (is_dirty(effect)) {
if ((effect.f & BLOCK_EFFECT) !== 0) this.#block_effects.push(effect); if ((effect.f & BLOCK_EFFECT) !== 0) this.#block_effects.push(effect);
update_effect(effect); update_effect(effect);
} }
@ -276,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 * Associate a change to a given source with the current
* batch, noting its previous and current values * batch, noting its previous and current values
@ -287,7 +350,7 @@ export class Batch {
this.#previous.set(source, value); this.#previous.set(source, value);
} }
this.#current.set(source, source.v); this.current.set(source, source.v);
} }
activate() { activate() {
@ -296,6 +359,7 @@ export class Batch {
deactivate() { deactivate() {
current_batch = null; current_batch = null;
previous_batch = null;
for (const update of effect_pending_updates) { for (const update of effect_pending_updates) {
effect_pending_updates.delete(update); effect_pending_updates.delete(update);
@ -314,13 +378,13 @@ export class Batch {
flush() { flush() {
if (queued_root_effects.length > 0) { if (queued_root_effects.length > 0) {
this.flush_effects(); flush_effects();
} else { } else {
this.#commit(); this.#commit();
} }
if (current_batch !== this) { 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 // which is permitted in legacy mode despite being a terrible idea
return; return;
} }
@ -332,52 +396,6 @@ export class Batch {
this.deactivate(); 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 * Append and remove branches to/from the DOM
*/ */
@ -399,18 +417,13 @@ export class Batch {
this.#pending -= 1; this.#pending -= 1;
if (this.#pending === 0) { if (this.#pending === 0) {
for (const e of this.#render_effects) { for (const e of this.#dirty_effects) {
set_signal_status(e, DIRTY); set_signal_status(e, DIRTY);
schedule_effect(e); schedule_effect(e);
} }
for (const e of this.#effects) { for (const e of this.#maybe_dirty_effects) {
set_signal_status(e, DIRTY); set_signal_status(e, MAYBE_DIRTY);
schedule_effect(e);
}
for (const e of this.#block_effects) {
set_signal_status(e, DIRTY);
schedule_effect(e); schedule_effect(e);
} }
@ -432,13 +445,13 @@ export class Batch {
return (this.#deferred ??= deferred()).promise; return (this.#deferred ??= deferred()).promise;
} }
static ensure(autoflush = true) { static ensure() {
if (current_batch === null) { if (current_batch === null) {
const batch = (current_batch = new Batch()); const batch = (current_batch = new Batch());
batches.add(current_batch); batches.add(current_batch);
if (autoflush) { if (!is_flushing_sync) {
queueMicrotask(() => { Batch.enqueue(() => {
if (current_batch !== batch) { if (current_batch !== batch) {
// a flushSync happened in the meantime // a flushSync happened in the meantime
return; return;
@ -451,6 +464,15 @@ export class Batch {
return current_batch; return current_batch;
} }
/** @param {() => void} task */
static enqueue(task) {
if (tasks.length === 0) {
queueMicrotask(dequeue);
}
tasks.unshift(task);
}
} }
/** /**
@ -462,35 +484,89 @@ export class Batch {
*/ */
export function flushSync(fn) { export function flushSync(fn) {
if (async_mode_flag && active_effect !== null) { if (async_mode_flag && active_effect !== null) {
// We disallow this because it creates super-hard to reason about stack trace and because it's generally a bad idea
e.flush_sync_in_effect(); 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) { if (fn) {
batch.flush_effects(); flush_effects();
result = fn();
}
result = fn(); while (true) {
} flush_tasks();
while (true) { if (queued_root_effects.length === 0) {
flush_tasks(); current_batch?.flush();
if (queued_root_effects.length === 0) { // we need to check again, in case we just updated an `$effect.pending()`
if (batch === current_batch) { if (queued_root_effects.length === 0) {
batch.flush(); // 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, flush_effects();
// we need to reset it here as well in case the first time there's 0 queued root effects }
last_scheduled_effect = null; } 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();
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();
}
return /** @type {T} */ (result); 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;
} }
} }
@ -509,6 +585,9 @@ function infinite_loop_guard() {
} }
} }
/** @type {Effect[] | null} */
export let eager_block_effects = null;
/** /**
* @param {Array<Effect>} effects * @param {Array<Effect>} effects
* @returns {void} * @returns {void}
@ -517,44 +596,49 @@ function flush_queued_effects(effects) {
var length = effects.length; var length = effects.length;
if (length === 0) return; if (length === 0) return;
for (var i = 0; i < length; i++) { var i = 0;
var effect = effects[i];
while (i < length) {
if ((effect.f & (DESTROYED | INERT)) === 0) { var effect = effects[i++];
if (is_dirty(effect)) {
var wv = write_version; if ((effect.f & (DESTROYED | INERT)) === 0 && is_dirty(effect)) {
eager_block_effects = [];
update_effect(effect);
update_effect(effect);
// Effects with no dependencies or teardown do not get added to the effect tree.
// Deferred effects (e.g. `$effect(...)`) _are_ added to the tree because we // Effects with no dependencies or teardown do not get added to the effect tree.
// don't know if we need to keep them until they are executed. Doing the check // Deferred effects (e.g. `$effect(...)`) _are_ added to the tree because we
// here (rather than in `update_effect`) allows us to skip the work for // don't know if we need to keep them until they are executed. Doing the check
// immediate effects. // here (rather than in `update_effect`) allows us to skip the work for
if (effect.deps === null && effect.first === null && effect.nodes_start === null) { // immediate effects.
// if there's no teardown or abort controller we completely unlink if (effect.deps === null && effect.first === null && effect.nodes_start === null) {
// the effect from the graph // if there's no teardown or abort controller we completely unlink
if (effect.teardown === null && effect.ac === null) { // the effect from the graph
// remove this effect from the graph if (effect.teardown === null && effect.ac === null) {
unlink_effect(effect); // remove this effect from the graph
} else { unlink_effect(effect);
// keep the effect in the graph, but free up some memory } else {
effect.fn = null; // keep the effect in the graph, but free up some memory
} effect.fn = null;
} }
}
// If update_effect() has a flushSync() in it, we may have flushed another flush_queued_effects(),
// which already handled this logic and did set eager_block_effects to null.
if (eager_block_effects?.length > 0) {
// TODO this feels incorrect! it gets the tests passing
old_values.clear();
// if state is written in a user effect, abort and re-schedule, lest we run for (const e of eager_block_effects) {
// effects that should be removed as a result of the state change update_effect(e);
if (write_version > wv && (effect.f & USER_EFFECT) !== 0) {
break;
} }
eager_block_effects = [];
} }
} }
} }
for (; i < length; i += 1) { eager_block_effects = null;
schedule_effect(effects[i]);
}
} }
/** /**
@ -593,7 +677,13 @@ export function suspend() {
return function unsuspend() { return function unsuspend() {
boundary.update_pending_count(-1); boundary.update_pending_count(-1);
if (!pending) batch.decrement();
if (!pending) {
batch.activate();
batch.decrement();
} else {
batch.deactivate();
}
unset_context(); unset_context();
}; };

@ -120,6 +120,9 @@ export function async_derived(fn, location) {
try { try {
var p = fn(); var p = fn();
// Make sure to always access the then property to read any signals
// it might access, so that we track them as dependencies.
if (prev) Promise.resolve(p).catch(() => {}); // avoid unhandled rejection
} catch (error) { } catch (error) {
p = Promise.reject(error); p = Promise.reject(error);
} }
@ -337,7 +340,9 @@ export function update_derived(derived) {
// don't mark derived clean if we're reading it inside a // don't mark derived clean if we're reading it inside a
// cleanup function, or it will cache a stale value // cleanup function, or it will cache a stale value
if (is_destroying_effect) return; if (is_destroying_effect) {
return;
}
if (batch_deriveds !== null) { if (batch_deriveds !== null) {
batch_deriveds.set(derived, derived.v); batch_deriveds.set(derived, derived.v);

@ -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 { component_context, dev_current_component_function, dev_stack } from '../context.js';
import { Batch, schedule_effect } from './batch.js'; import { Batch, schedule_effect } from './batch.js';
import { flatten } from './async.js'; import { flatten } from './async.js';
import { without_reactive_context } from '../dom/elements/bindings/shared.js';
const VALID_EFFECT_PARENT = 0; const VALID_EFFECT_PARENT = 0;
const EFFECT_ORPHAN = 1; const EFFECT_ORPHAN = 1;
@ -161,25 +162,40 @@ function create_effect(type, fn, sync, push = true) {
schedule_effect(effect); schedule_effect(effect);
} }
// if an effect has no dependencies, no DOM and no teardown function, if (push) {
// don't bother adding it to the effect tree /** @type {Effect | null} */
var inert = var e = effect;
sync &&
effect.deps === null && // if an effect has already ran and doesn't need to be kept in the tree
effect.first === null && // (because it won't re-run, has no DOM, and has no teardown etc)
effect.nodes_start === null && // then we skip it and go to its child (if any)
effect.teardown === null && if (
(effect.f & EFFECT_PRESERVED) === 0; sync &&
e.deps === null &&
if (!inert && push) { e.teardown === null &&
if (parent !== null) { e.nodes_start === null &&
push_effect(effect, parent); 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 (e !== null) {
if (active_reaction !== null && (active_reaction.f & DERIVED) !== 0) { e.parent = parent;
var derived = /** @type {Derived} */ (active_reaction);
(derived.effects ??= []).push(effect); 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);
}
} }
} }
@ -274,7 +290,7 @@ export function inspect_effect(fn) {
*/ */
export function effect_root(fn) { export function effect_root(fn) {
Batch.ensure(); Batch.ensure();
const effect = create_effect(ROOT_EFFECT, fn, true); const effect = create_effect(ROOT_EFFECT | EFFECT_PRESERVED, fn, true);
return () => { return () => {
destroy_effect(effect); destroy_effect(effect);
@ -288,7 +304,7 @@ export function effect_root(fn) {
*/ */
export function component_root(fn) { export function component_root(fn) {
Batch.ensure(); Batch.ensure();
const effect = create_effect(ROOT_EFFECT, fn, true); const effect = create_effect(ROOT_EFFECT | EFFECT_PRESERVED, fn, true);
return (options = {}) => { return (options = {}) => {
return new Promise((fulfil) => { return new Promise((fulfil) => {
@ -407,7 +423,7 @@ export function block(fn, flags = 0) {
* @param {boolean} [push] * @param {boolean} [push]
*/ */
export function branch(fn, push = true) { export function branch(fn, push = true) {
return create_effect(BRANCH_EFFECT, fn, true, push); return create_effect(BRANCH_EFFECT | EFFECT_PRESERVED, fn, true, push);
} }
/** /**
@ -439,7 +455,13 @@ export function destroy_effect_children(signal, remove_dom = false) {
signal.first = signal.last = null; signal.first = signal.last = null;
while (effect !== 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; var next = effect.next;
@ -674,7 +696,6 @@ function resume_children(effect, local) {
} }
} }
export function aborted() { export function aborted(effect = /** @type {Effect} */ (active_effect)) {
var effect = /** @type {Effect} */ (active_effect);
return (effect.f & DESTROYED) !== 0; return (effect.f & DESTROYED) !== 0;
} }

@ -1,13 +1,11 @@
/** @import { ComponentContext } from '#client' */ /** @import { Effect, Source } from './types.js' */
/** @import { Derived, Effect, Source } from './types.js' */
import { DEV } from 'esm-env'; import { DEV } from 'esm-env';
import { import {
PROPS_IS_BINDABLE, PROPS_IS_BINDABLE,
PROPS_IS_IMMUTABLE, PROPS_IS_IMMUTABLE,
PROPS_IS_LAZY_INITIAL, PROPS_IS_LAZY_INITIAL,
PROPS_IS_RUNES, PROPS_IS_RUNES,
PROPS_IS_UPDATED, PROPS_IS_UPDATED
UNINITIALIZED
} from '../../../constants.js'; } from '../../../constants.js';
import { get_descriptor, is_function } from '../../shared/utils.js'; import { get_descriptor, is_function } from '../../shared/utils.js';
import { set, source, update } from './sources.js'; import { set, source, update } from './sources.js';
@ -186,8 +184,7 @@ export function legacy_rest_props(props, exclude) {
* The proxy handler for spread props. Handles the incoming array of props * The proxy handler for spread props. Handles the incoming array of props
* that looks like `() => { dynamic: props }, { static: prop }, ..` and wraps * that looks like `() => { dynamic: props }, { static: prop }, ..` and wraps
* them so that the whole thing is passed to the component as the `$$props` argument. * them so that the whole thing is passed to the component as the `$$props` argument.
* @template {Record<string | symbol, unknown>} T * @type {ProxyHandler<{ props: Array<Record<string | symbol, unknown> | (() => Record<string | symbol, unknown>)> }>}}
* @type {ProxyHandler<{ props: Array<T | (() => T)> }>}}
*/ */
const spread_props_handler = { const spread_props_handler = {
get(target, key) { get(target, key) {
@ -364,22 +361,23 @@ export function prop(props, key, flags, fallback) {
// means we can just call `$$props.foo = value` directly // means we can just call `$$props.foo = value` directly
if (setter) { if (setter) {
var legacy_parent = props.$$legacy; var legacy_parent = props.$$legacy;
return /** @type {() => V} */ (
return function (/** @type {any} */ value, /** @type {boolean} */ mutation) { function (/** @type {V} */ value, /** @type {boolean} */ mutation) {
if (arguments.length > 0) { if (arguments.length > 0) {
// We don't want to notify if the value was mutated and the parent is in runes mode. // 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. // 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 // 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. // has changed because the parent will not be able to detect the change otherwise.
if (!runes || !mutation || legacy_parent || is_store_sub) { if (!runes || !mutation || legacy_parent || is_store_sub) {
/** @type {Function} */ (setter)(mutation ? getter() : value); /** @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 // Either prop is written to, but there's no binding, which means we
@ -393,34 +391,40 @@ export function prop(props, key, flags, fallback) {
return getter(); return getter();
}); });
if (DEV) {
d.label = key;
}
// Capture the initial value if it's bindable // Capture the initial value if it's bindable
if (bindable) get(d); if (bindable) get(d);
var parent_effect = /** @type {Effect} */ (active_effect); var parent_effect = /** @type {Effect} */ (active_effect);
return function (/** @type {any} */ value, /** @type {boolean} */ mutation) { return /** @type {() => V} */ (
if (arguments.length > 0) { function (/** @type {any} */ value, /** @type {boolean} */ mutation) {
const new_value = mutation ? get(d) : runes && bindable ? proxy(value) : value; if (arguments.length > 0) {
const new_value = mutation ? get(d) : runes && bindable ? proxy(value) : value;
set(d, new_value); set(d, new_value);
overridden = true; overridden = true;
if (fallback_value !== undefined) { if (fallback_value !== undefined) {
fallback_value = new_value; 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 return get(d);
// 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);
};
} }

@ -33,10 +33,11 @@ import * as e from '../errors.js';
import { legacy_mode_flag, tracing_mode_flag } from '../../flags/index.js'; import { legacy_mode_flag, tracing_mode_flag } from '../../flags/index.js';
import { get_stack, tag_proxy } from '../dev/tracing.js'; import { get_stack, tag_proxy } from '../dev/tracing.js';
import { component_context, is_runes } from '../context.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 { proxy } from '../proxy.js';
import { execute_derived } from './deriveds.js'; import { execute_derived } from './deriveds.js';
/** @type {Set<any>} */
export let inspect_effects = new Set(); export let inspect_effects = new Set();
/** @type {Map<Source, any>} */ /** @type {Map<Source, any>} */
@ -179,7 +180,7 @@ export function internal_set(source, value) {
source.v = value; source.v = value;
const batch = Batch.ensure(); var batch = Batch.ensure();
batch.capture(source, old_value); batch.capture(source, old_value);
if (DEV) { if (DEV) {
@ -314,9 +315,6 @@ function mark_reactions(signal, status) {
var reaction = reactions[i]; var reaction = reactions[i];
var flags = reaction.f; var flags = reaction.f;
// Skip any effects that are already dirty
if ((flags & DIRTY) !== 0) continue;
// In legacy mode, skip the current effect to prevent infinite loops // In legacy mode, skip the current effect to prevent infinite loops
if (!runes && reaction === active_effect) continue; if (!runes && reaction === active_effect) continue;
@ -326,15 +324,23 @@ function mark_reactions(signal, status) {
continue; continue;
} }
set_signal_status(reaction, status); var not_dirty = (flags & DIRTY) === 0;
// If the signal a) was previously clean or b) is an unowned derived, then mark it // don't set a DIRTY reaction to MAYBE_DIRTY
if ((flags & (CLEAN | UNOWNED)) !== 0) { if (not_dirty) {
if ((flags & DERIVED) !== 0) { set_signal_status(reaction, status);
mark_reactions(/** @type {Derived} */ (reaction), MAYBE_DIRTY); }
} else {
schedule_effect(/** @type {Effect} */ (reaction)); if ((flags & DERIVED) !== 0) {
mark_reactions(/** @type {Derived} */ (reaction), MAYBE_DIRTY);
} 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));
} }
} }
} }

@ -6,6 +6,7 @@ import { define_property, noop } from '../../shared/utils.js';
import { get } from '../runtime.js'; import { get } from '../runtime.js';
import { teardown } from './effects.js'; import { teardown } from './effects.js';
import { mutable_source, set } from './sources.js'; import { mutable_source, set } from './sources.js';
import { DEV } from 'esm-env';
/** /**
* Whether or not the prop currently being read is a store binding, as in * Whether or not the prop currently being read is a store binding, as in
@ -33,6 +34,10 @@ export function store_get(store, store_name, stores) {
unsubscribe: noop unsubscribe: noop
}); });
if (DEV) {
entry.source.label = store_name;
}
// if the component that setup this is already unmounted we don't want to register a subscription // if the component that setup this is already unmounted we don't want to register a subscription
if (entry.store !== store && !(IS_UNMOUNTED in stores)) { if (entry.store !== store && !(IS_UNMOUNTED in stores)) {
entry.unsubscribe(); entry.unsubscribe();

@ -54,7 +54,7 @@ export interface Reaction extends Signal {
export interface Derived<V = unknown> extends Value<V>, Reaction { export interface Derived<V = unknown> extends Value<V>, Reaction {
/** The derived function */ /** The derived function */
fn: () => V; 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[]; effects: null | Effect[];
/** Parent effect or derived */ /** Parent effect or derived */
parent: Effect | Derived | null; parent: Effect | Derived | null;

@ -136,20 +136,28 @@ export function hydrate(component, options) {
return /** @type {Exports} */ (instance); return /** @type {Exports} */ (instance);
} catch (error) { } catch (error) {
if (error === HYDRATION_ERROR) { // re-throw Svelte errors - they are certainly not related to hydration
if (options.recover === false) { if (
e.hydration_failed(); error instanceof Error &&
} error.message.split('\n').some((line) => line.startsWith('https://svelte.dev/e/'))
) {
// If an error occured above, the operations might not yet have been initialised. throw error;
init_operations(); }
clear_text_content(target); if (error !== HYDRATION_ERROR) {
// eslint-disable-next-line no-console
console.warn('Failed to hydrate: ', error);
}
set_hydrating(false); if (options.recover === false) {
return mount(component, options); 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 { } finally {
set_hydrating(was_hydrating); set_hydrating(was_hydrating);
set_hydrate_node(previous_hydrate_node); 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 }) { function _mount(Component, { target, anchor, props = {}, events, context, intro = true }) {
init_operations(); init_operations();
/** @type {Set<string>} */
var registered_events = new Set(); var registered_events = new Set();
/** @param {Array<string>} events */ /** @param {Array<string>} events */

@ -22,7 +22,7 @@ import {
STALE_REACTION, STALE_REACTION,
ERROR_VALUE ERROR_VALUE
} from './constants.js'; } from './constants.js';
import { internal_set, old_values } from './reactivity/sources.js'; import { old_values } from './reactivity/sources.js';
import { import {
destroy_derived_effects, destroy_derived_effects,
execute_derived, execute_derived,
@ -45,6 +45,8 @@ import * as w from './warnings.js';
import { Batch, batch_deriveds, flushSync, schedule_effect } from './reactivity/batch.js'; import { Batch, batch_deriveds, flushSync, schedule_effect } from './reactivity/batch.js';
import { handle_error } from './error-handling.js'; import { handle_error } from './error-handling.js';
import { UNINITIALIZED } from '../../constants.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; export let is_updating_effect = false;
@ -137,14 +139,6 @@ export function set_update_version(value) {
// If we are working with a get() chain that has no active container, // If we are working with a get() chain that has no active container,
// to prevent memory leaks, we skip adding the reaction. // to prevent memory leaks, we skip adding the reaction.
export let skip_reaction = false; export let skip_reaction = false;
// Handle collecting all signals which are read during a specific time frame
/** @type {Set<Value> | null} */
export let captured_signals = null;
/** @param {Set<Value> | null} value */
export function set_captured_signals(value) {
captured_signals = value;
}
export function increment_write_version() { export function increment_write_version() {
return ++write_version; return ++write_version;
@ -285,13 +279,17 @@ export function update_reaction(reaction) {
update_version = ++read_version; update_version = ++read_version;
if (reaction.ac !== null) { if (reaction.ac !== null) {
reaction.ac.abort(STALE_REACTION); without_reactive_context(() => {
/** @type {AbortController} */ (reaction.ac).abort(STALE_REACTION);
});
reaction.ac = null; reaction.ac = null;
} }
try { try {
reaction.f |= REACTION_IS_UPDATING; 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; var deps = reaction.deps;
if (new_deps !== null) { if (new_deps !== null) {
@ -531,9 +529,7 @@ export function get(signal) {
var flags = signal.f; var flags = signal.f;
var is_derived = (flags & DERIVED) !== 0; var is_derived = (flags & DERIVED) !== 0;
if (captured_signals !== null) { captured_signals?.add(signal);
captured_signals.add(signal);
}
// Register the dependency on the current reaction signal. // Register the dependency on the current reaction signal.
if (active_reaction !== null && !untracking) { if (active_reaction !== null && !untracking) {
@ -713,45 +709,6 @@ export function safe_get(signal) {
return signal && get(signal); return signal && get(signal);
} }
/**
* Capture an array of all the signals that are read when `fn` is called
* @template T
* @param {() => T} fn
*/
function capture_signals(fn) {
var previous_captured_signals = captured_signals;
captured_signals = new Set();
var captured = captured_signals;
var signal;
try {
untrack(fn);
if (previous_captured_signals !== null) {
for (signal of captured_signals) {
previous_captured_signals.add(signal);
}
}
} finally {
captured_signals = previous_captured_signals;
}
return captured;
}
/**
* Invokes a function and captures all signals that are read during the invocation,
* then invalidates them.
* @param {() => any} fn
*/
export function invalidate_inner_signals(fn) {
var captured = capture_signals(() => untrack(fn));
for (var signal of captured) {
internal_set(signal, signal.v);
}
}
/** /**
* When used inside a [`$derived`](https://svelte.dev/docs/svelte/$derived) or [`$effect`](https://svelte.dev/docs/svelte/$effect), * When used inside a [`$derived`](https://svelte.dev/docs/svelte/$derived) or [`$effect`](https://svelte.dev/docs/svelte/$effect),
* any state read inside `fn` will not be treated as a dependency. * any state read inside `fn` will not be treated as a dependency.

@ -6,7 +6,12 @@ export class HeadPayload {
uid = () => ''; uid = () => '';
title = ''; 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.css = css;
this.out = out; this.out = out;
this.title = title; this.title = title;

@ -15,14 +15,15 @@ const empty = [];
* @template T * @template T
* @param {T} value * @param {T} value
* @param {boolean} [skip_warning] * @param {boolean} [skip_warning]
* @param {boolean} [no_tojson]
* @returns {Snapshot<T>} * @returns {Snapshot<T>}
*/ */
export function snapshot(value, skip_warning = false) { export function snapshot(value, skip_warning = false, no_tojson = false) {
if (DEV && !skip_warning) { if (DEV && !skip_warning) {
/** @type {string[]} */ /** @type {string[]} */
const paths = []; 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] === '') { if (paths.length === 1 && paths[0] === '') {
// value could not be cloned // value could not be cloned
w.state_snapshot_uncloneable(); w.state_snapshot_uncloneable();
@ -40,7 +41,7 @@ export function snapshot(value, skip_warning = false) {
return copy; 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<T, Snapshot<T>>} cloned * @param {Map<T, Snapshot<T>>} cloned
* @param {string} path * @param {string} path
* @param {string[]} paths * @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<T>} * @returns {Snapshot<T>}
*/ */
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) { if (typeof value === 'object' && value !== null) {
var unwrapped = cloned.get(value); var unwrapped = cloned.get(value);
if (unwrapped !== undefined) return unwrapped; 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) { for (var i = 0; i < value.length; i += 1) {
var element = value[i]; var element = value[i];
if (i in value) { 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) { for (var key in value) {
// @ts-expect-error copy[key] = clone(
copy[key] = clone(value[key], cloned, DEV ? `${path}.${key}` : path, paths); // @ts-expect-error
value[key],
cloned,
DEV ? `${path}.${key}` : path,
paths,
null,
no_tojson
);
} }
return copy; return copy;
@ -99,7 +108,7 @@ function clone(value, cloned, path, paths, original = null) {
return /** @type {Snapshot<T>} */ (structuredClone(value)); return /** @type {Snapshot<T>} */ (structuredClone(value));
} }
if (typeof (/** @type {T & { toJSON?: any } } */ (value).toJSON) === 'function') { if (typeof (/** @type {T & { toJSON?: any } } */ (value).toJSON) === 'function' && !no_tojson) {
return clone( return clone(
/** @type {T & { toJSON(): any } } */ (value).toJSON(), /** @type {T & { toJSON(): any } } */ (value).toJSON(),
cloned, cloned,

@ -35,7 +35,7 @@ export function validate_store(store, name) {
} }
/** /**
* @template {() => unknown} T * @template {(...args: any[]) => unknown} T
* @param {T} fn * @param {T} fn
*/ */
export function prevent_snippet_stringification(fn) { export function prevent_snippet_stringification(fn) {

@ -82,6 +82,10 @@ export function createSubscriber(start) {
if (subscribers === 0) { if (subscribers === 0) {
stop?.(); stop?.();
stop = undefined; stop = undefined;
// Increment the version to ensure any dependent deriveds are marked dirty when the subscription is picked up again later.
// If we didn't do this then the comparison of write versions would determine that the derived has a later version than
// the subscriber, and it would not be re-run.
increment(version);
} }
}); });
}; };

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save