Merge branch 'main' into effect-active

effect-active
ComputerGuy 5 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:
CI: true
SVELTE_NO_ASYNC: true
TSGo:
permissions: {}
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 24
cache: pnpm
- name: install
run: pnpm install --frozen-lockfile
- name: install tsgo
run: cd packages/svelte && pnpm i -D @typescript/native-preview
- name: type check
run: cd packages/svelte && pnpm check:tsgo
Lint:
permissions: {}
runs-on: ubuntu-latest

@ -101,13 +101,13 @@ Test samples are kept in `/test/xxx/samples` folder.
1. To run test, run `pnpm test`.
1. To run a particular test suite, use `pnpm test <suite-name>`, for example:
```bash
```sh
pnpm test validator
```
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
```

@ -4,7 +4,7 @@ title: Getting started
We recommend using [SvelteKit](../kit), which lets you [build almost anything](../kit/project-types). It's the official application framework from the Svelte team and powered by [Vite](https://vite.dev/). Create a new project with:
```bash
```sh
npx sv create myapp
cd myapp
npm install

@ -135,7 +135,7 @@ An effect only reruns when the object it reads changes, not when a property insi
An effect only depends on the values that it read the last time it ran. This has interesting implications for effects that have conditional code.
For instance, if `condition` is `true` in the code snippet below, the code inside the `if` block will run and `color` will be evaluated. As such, changes to either `condition` or `color` [will cause the effect to re-run](/playground/untitled#H4sIAAAAAAAAE21RQW6DMBD8ytaNBJHaJFLViwNIVZ8RcnBgXVk1xsILTYT4e20TQg89IOPZ2fHM7siMaJBx9tmaWpFqjQNlAKXEihx7YVJpdIyfRkY3G4gB8Pi97cPanRtQU8AuwuF_eNUaQuPlOMtc1SlLRWlKUo1tOwJflUikQHZtA0klzCDc64Imx0ANn8bInV1CDhtHgjClrsftcSXotluLybOUb3g4JJHhOZs5WZpuIS9gjNqkJKQP5e2ClrR4SMdZ13E4xZ8zTPOTJU2A2uE_PQ9COCI926_hTVarIU4hu_REPlBrKq2q73ycrf1N-vS4TMUsulaVg3EtR8H9rFgsg8uUsT1B2F9eshigZHBRpuaD0D3mY8Qm2BfB5N2YyRzdNEYVDy0Ja-WsFjcOUuP1HvFLWA6H3XuHTUSmmDV2--0TXonxsKbp7G9C6R__NONS-MFNvxj_d6mBAgAA).
For instance, if `condition` is `true` in the code snippet below, the code inside the `if` block will run and `color` will be evaluated. This means that changes to either `condition` or `color` [will cause the effect to re-run](/playground/untitled#H4sIAAAAAAAAE21RQW6DMBD8ytaNBJHaJFLViwNIVZ8RcnBgXVk1xsILTYT4e20TQg89IOPZ2fHM7siMaJBx9tmaWpFqjQNlAKXEihx7YVJpdIyfRkY3G4gB8Pi97cPanRtQU8AuwuF_eNUaQuPlOMtc1SlLRWlKUo1tOwJflUikQHZtA0klzCDc64Imx0ANn8bInV1CDhtHgjClrsftcSXotluLybOUb3g4JJHhOZs5WZpuIS9gjNqkJKQP5e2ClrR4SMdZ13E4xZ8zTPOTJU2A2uE_PQ9COCI926_hTVarIU4hu_REPlBrKq2q73ycrf1N-vS4TMUsulaVg3EtR8H9rFgsg8uUsT1B2F9eshigZHBRpuaD0D3mY8Qm2BfB5N2YyRzdNEYVDy0Ja-WsFjcOUuP1HvFLWA6H3XuHTUSmmDV2--0TXonxsKbp7G9C6R__NONS-MFNvxj_d6mBAgAA).
Conversely, if `condition` is `false`, `color` will not be evaluated, and the effect will _only_ re-run again when `condition` changes.

@ -277,4 +277,4 @@ Snippets can be created programmatically with the [`createRawSnippet`](svelte#cr
## Snippets and slots
In Svelte 4, content can be passed to components using [slots](legacy-slots). Snippets are more powerful and flexible, and as such slots are deprecated in Svelte 5.
In Svelte 4, content can be passed to components using [slots](legacy-slots). Snippets are more powerful and flexible, and so slots have been deprecated in Svelte 5.

@ -22,7 +22,7 @@ It also will not compile Svelte code.
## Styling
Content rendered this way is 'invisible' to Svelte and as such will not receive [scoped styles](scoped-styles) — in other words, this will not work, and the `a` and `img` styles will be regarded as unused:
Content rendered this way is 'invisible' to Svelte and as such will not receive [scoped styles](scoped-styles). In other words, this will not work, and the `a` and `img` styles will be regarded as unused:
<!-- prettier-ignore -->
```svelte

@ -71,7 +71,7 @@ The user of this component has the same flexibility to use a mixture of objects,
</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
<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_
- `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
- `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
> 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>
```
> [!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`

@ -10,7 +10,7 @@ Unit tests allow you to test small isolated parts of your code. Integration test
To setup Vitest manually, first install it:
```bash
```sh
npm install -D vitest
```
@ -166,7 +166,7 @@ It is possible to test your components in isolation using Vitest.
To get started, install jsdom (a library that shims DOM APIs):
```bash
```sh
npm install -D jsdom
```

@ -245,7 +245,7 @@ In Svelte 4, you can add event modifiers to handlers:
<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:
@ -340,7 +340,7 @@ When spreading props, local event handlers must go _after_ the spread, or they r
## Snippets instead of slots
In Svelte 4, content can be passed to components using slots. Svelte 5 replaces them with snippets which are more powerful and flexible, and as such slots are deprecated in Svelte 5.
In Svelte 4, content can be passed to components using slots. Svelte 5 replaces them with snippets, which are more powerful and flexible, and so slots are deprecated in Svelte 5.
They continue to work, however, and you can pass snippets to a component that uses slots:
@ -599,7 +599,7 @@ Note that `mount` and `hydrate` are _not_ synchronous, so things like `onMount`
### Server API changes
Similarly, components no longer have a `render` method when compiled for server side rendering. Instead, pass the function to `render` from `svelte/server`:
Similarly, components no longer have a `render` method when compiled for server-side rendering. Instead, pass the function to `render` from `svelte/server`:
```js
+++import { render } from 'svelte/server';+++
@ -803,7 +803,7 @@ Note that Svelte 5 will also warn if you have a single expression wrapped in quo
### HTML structure is stricter
In Svelte 4, you were allowed to write HTML code that would be repaired by the browser when server side rendering it. For example you could write this...
In Svelte 4, you were allowed to write HTML code that would be repaired by the browser when server-side rendering it. For example you could write this...
```svelte
<table>
@ -835,7 +835,7 @@ Assignments to destructured parts of a `@const` declaration are no longer allowe
### :is(...), :has(...), and :where(...) are scoped
Previously, Svelte did not analyse selectors inside `:is(...)`, `:has(...)`, and `:where(...)`, effectively treating them as global. Svelte 5 analyses them in the context of the current component. As such, some selectors may now be treated as unused if they were relying on this treatment. To fix this, use `:global(...)` inside the `:is(...)/:has(...)/:where(...)` selectors.
Previously, Svelte did not analyse selectors inside `:is(...)`, `:has(...)`, and `:where(...)`, effectively treating them as global. Svelte 5 analyses them in the context of the current component. Some selectors may now therefore be treated as unused if they were relying on this treatment. To fix this, use `:global(...)` inside the `:is(...)/:has(...)/:where(...)` selectors.
When using Tailwind's `@apply` directive, add a `:global` selector to preserve rules that use Tailwind-generated `:is(...)` selectors:
@ -964,7 +964,7 @@ Since these mismatches are extremely rare, Svelte 5 assumes that the values are
### Hydration works differently
Svelte 5 makes use of comments during server side rendering which are used for more robust and efficient hydration on the client. As such, you shouldn't remove comments from your HTML output if you intend to hydrate it, and if you manually authored HTML to be hydrated by a Svelte component, you need to adjust that HTML to include said comments at the correct positions.
Svelte 5 makes use of comments during server-side rendering which are used for more robust and efficient hydration on the client. You therefore should not remove comments from your HTML output if you intend to hydrate it, and if you manually authored HTML to be hydrated by a Svelte component, you need to adjust that HTML to include said comments at the correct positions.
### `onevent` attributes are delegated

@ -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_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
```
@ -364,6 +409,12 @@ The $ name is reserved, and cannot be used for variables and imports
The $ prefix is reserved, and cannot be used for variables and imports
```
### duplicate_class_field
```
`%name%` has already been declared
```
### each_item_invalid_assignment
```

@ -679,11 +679,11 @@ In HTML, there's [no such thing as a self-closing tag](https://jakearchibald.com
</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:
```bash
```sh
npx sv migrate self-closing-tags
```

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

@ -1,5 +1,197 @@
# 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
### 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):
```bash
```sh
npx sv create my-app
cd my-app
npm install
@ -30,7 +30,7 @@ See [the SvelteKit documentation](https://svelte.dev/docs/kit) to learn more.
## Changelog
[The Changelog for this package is available on GitHub](https://github.com/sveltejs/svelte/blob/master/packages/svelte/CHANGELOG.md).
[The Changelog for this package is available on GitHub](https://github.com/sveltejs/svelte/blob/main/packages/svelte/CHANGELOG.md).
## Supporting Svelte

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

@ -30,6 +30,10 @@
> The $ prefix is reserved, and cannot be used for variables and imports
## duplicate_class_field
> `%name%` has already been declared
## each_item_invalid_assignment
> Cannot reassign or bind to each block argument in runes mode. Use the array and index variables instead (e.g. `array[i] = value` instead of `entry = value`, or `bind:value={array[i]}` instead of `bind:value={entry}`)

@ -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_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 ...} 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>
```
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:
```bash
```sh
npx sv migrate self-closing-tags
```

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

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

@ -229,7 +229,7 @@ declare namespace $derived {
*
* If you return a function from the effect, it will be called right before the effect is run again, or when the component is unmounted.
*
* Does not run during server side rendering.
* Does not run during server-side rendering.
*
* https://svelte.dev/docs/svelte/$effect
* @param fn The function to execute
@ -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.
*
* Does not run during server side rendering.
* Does not run during server-side rendering.
*
* https://svelte.dev/docs/svelte/$effect#$effect.pre
* @param fn The function to execute

@ -152,6 +152,16 @@ export function dollar_prefix_invalid(node) {
e(node, 'dollar_prefix_invalid', `The $ prefix is reserved, and cannot be used for variables and imports\nhttps://svelte.dev/e/dollar_prefix_invalid`);
}
/**
* `%name%` has already been declared
* @param {null | number | NodeLike} node
* @param {string} name
* @returns {never}
*/
export function duplicate_class_field(node, name) {
e(node, 'duplicate_class_field', `\`${name}\` has already been declared\nhttps://svelte.dev/e/duplicate_class_field`);
}
/**
* Cannot reassign or bind to each block argument in runes mode. Use the array and index variables instead (e.g. `array[i] = value` instead of `entry = value`, or `bind:value={array[i]}` instead of `bind:value={entry}`)
* @param {null | number | NodeLike} node
@ -975,6 +985,16 @@ export function const_tag_invalid_placement(node) {
e(node, 'const_tag_invalid_placement', `\`{@const}\` must be the immediate child of \`{#snippet}\`, \`{#if}\`, \`{:else if}\`, \`{:else}\`, \`{#each}\`, \`{:then}\`, \`{:catch}\`, \`<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
* @param {null | number | NodeLike} node

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

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

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

@ -370,14 +370,6 @@ export default function element(parser) {
// ... or we're followed by whitespace, for example near the end of the template,
// which we want to take in so that language tools has more room to work with
parser.allow_whitespace();
if (parser.index === parser.template.length) {
while (
parser.index < parser.template_untrimmed.length &&
regex_whitespace.test(parser.template_untrimmed[parser.index])
) {
parser.index++;
}
}
}
}
}

@ -10,7 +10,8 @@ export function create_fragment(transparent = false) {
nodes: [],
metadata: {
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 { get_attribute_chunks, is_text_attribute } from '../../../utils/ast.js';
/** @typedef {NODE_PROBABLY_EXISTS | NODE_DEFINITELY_EXISTS} NodeExistsValue */
/** @typedef {FORWARD | BACKWARD} Direction */
/** @typedef {typeof NODE_PROBABLY_EXISTS | typeof NODE_DEFINITELY_EXISTS} NodeExistsValue */
/** @typedef {typeof FORWARD | typeof BACKWARD} Direction */
const NODE_PROBABLY_EXISTS = 0;
const NODE_DEFINITELY_EXISTS = 1;

@ -37,6 +37,7 @@ import { ExportNamedDeclaration } from './visitors/ExportNamedDeclaration.js';
import { ExportSpecifier } from './visitors/ExportSpecifier.js';
import { ExpressionStatement } from './visitors/ExpressionStatement.js';
import { ExpressionTag } from './visitors/ExpressionTag.js';
import { Fragment } from './visitors/Fragment.js';
import { FunctionDeclaration } from './visitors/FunctionDeclaration.js';
import { FunctionExpression } from './visitors/FunctionExpression.js';
import { HtmlTag } from './visitors/HtmlTag.js';
@ -156,6 +157,7 @@ const visitors = {
ExportSpecifier,
ExpressionStatement,
ExpressionTag,
Fragment,
FunctionDeclaration,
FunctionExpression,
HtmlTag,
@ -295,11 +297,12 @@ export function analyze_module(source, options) {
// TODO the following are not needed for modules, but we have to pass them in order to avoid type error,
// and reducing the type would result in a lot of tedious type casts elsewhere - find a good solution one day
ast_type: /** @type {any} */ (null),
component_slots: new Set(),
component_slots: /** @type {Set<string>} */ (new Set()),
expression: null,
function_depth: 0,
has_props_rune: false,
options: /** @type {ValidatedCompileOptions} */ (options),
fragment: null,
parent_element: null,
reactive_statement: null
},
@ -451,6 +454,8 @@ export function analyze_component(root, source, options) {
}
}
const is_custom_element = !!options.customElementOptions || options.customElement;
// TODO remove all the ?? stuff, we don't need it now that we're validating the config
/** @type {ComponentAnalysis} */
const analysis = {
@ -500,13 +505,13 @@ export function analyze_component(root, source, options) {
needs_props: false,
event_directive_node: null,
uses_event_attributes: false,
custom_element: options.customElementOptions ?? options.customElement,
inject_styles: options.css === 'injected' || options.customElement,
accessors: options.customElement
? true
: (runes ? false : !!options.accessors) ||
// because $set method needs accessors
options.compatibility?.componentApi === 4,
custom_element: is_custom_element,
inject_styles: options.css === 'injected' || is_custom_element,
accessors:
is_custom_element ||
(runes ? false : !!options.accessors) ||
// because $set method needs accessors
options.compatibility?.componentApi === 4,
reactive_statements: new Map(),
binding_groups: new Map(),
slot_names: new Map(),
@ -524,7 +529,6 @@ export function analyze_component(root, source, options) {
has_global: false
},
source,
undefined_exports: new Map(),
snippet_renderers: new Map(),
snippets: new Set(),
async_deriveds: new Set()
@ -686,6 +690,7 @@ export function analyze_component(root, source, options) {
analysis,
options,
ast_type: ast === instance.ast ? 'instance' : ast === template.ast ? 'template' : 'module',
fragment: ast === template.ast ? ast : null,
parent_element: null,
has_props_rune: false,
component_slots: new Set(),
@ -751,6 +756,7 @@ export function analyze_component(root, source, options) {
scopes,
analysis,
options,
fragment: ast === template.ast ? ast : null,
parent_element: null,
has_props_rune: false,
ast_type: ast === instance.ast ? 'instance' : ast === template.ast ? 'template' : 'module',
@ -785,9 +791,15 @@ export function analyze_component(root, source, options) {
if (node.type === 'ExportNamedDeclaration' && node.specifiers !== null && node.source == null) {
for (const specifier of node.specifiers) {
if (specifier.local.type !== 'Identifier') continue;
const binding = analysis.module.scope.get(specifier.local.name);
if (!binding) e.export_undefined(specifier, specifier.local.name);
const name = specifier.local.name;
const binding = analysis.module.scope.get(name);
if (!binding) {
if ([...analysis.snippets].find((snippet) => snippet.expression.name === name)) {
e.snippet_invalid_export(specifier);
} else {
e.export_undefined(specifier, name);
}
}
}
}
}

@ -8,6 +8,7 @@ export interface AnalysisState {
analysis: ComponentAnalysis;
options: ValidatedCompileOptions;
ast_type: 'instance' | 'template' | 'module';
fragment: AST.Fragment | null;
/**
* Tag name of the parent element. `null` if the parent is `svelte:element`, `#snippet`, a component or the root.
* Parent doesn't necessarily mean direct path predecessor because there could be `#each`, `#if` etc in-between.

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

@ -11,6 +11,15 @@ export function AwaitExpression(node, context) {
if (context.state.expression) {
context.state.expression.has_await = true;
if (
context.state.fragment &&
// TODO there's probably a better way to do this
context.path.some((node) => node.type === 'ConstTag')
) {
context.state.fragment.metadata.has_await = true;
}
suspend = true;
}

@ -33,6 +33,9 @@ export function ClassBody(node, context) {
/** @type {Map<string, StateField>} */
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);
/** @type {MethodDefinition | null} */
@ -54,6 +57,14 @@ export function ClassBody(node, context) {
e.state_field_duplicate(node, name);
}
const _key = (node.type === 'AssignmentExpression' || !node.static ? '' : '@') + name;
const field = fields.get(_key);
// if there's already a method or assigned field, error
if (field && !(field.length === 1 && field[0] === 'prop')) {
e.duplicate_class_field(node, _key);
}
state_fields.set(name, {
node,
type: rune,
@ -67,10 +78,48 @@ export function ClassBody(node, context) {
for (const child of node.body) {
if (child.type === 'PropertyDefinition' && !child.computed && !child.static) {
handle(child, child.key, child.value);
const key = /** @type {string} */ (get_name(child.key));
const field = fields.get(key);
if (!field) {
fields.set(key, [child.value ? 'assigned_prop' : 'prop']);
continue;
}
e.duplicate_class_field(child, key);
}
if (child.type === 'MethodDefinition' && child.kind === 'constructor') {
constructor = child;
if (child.type === 'MethodDefinition') {
if (child.kind === 'constructor') {
constructor = child;
} else if (!child.computed) {
const key = (child.static ? '@' : '') + get_name(child.key);
const field = fields.get(key);
if (!field) {
fields.set(key, [child.kind]);
continue;
}
if (
field.includes(child.kind) ||
field.includes('prop') ||
field.includes('assigned_prop')
) {
e.duplicate_class_field(child, key);
}
if (child.kind === 'get') {
if (field.length === 1 && field[0] === 'set') {
field.push('get');
continue;
}
} else if (child.kind === 'set') {
if (field.length === 1 && field[0] === 'get') {
field.push('set');
continue;
}
} else {
field.push(child.kind);
continue;
}
e.duplicate_class_field(child, key);
}
}
}

@ -11,6 +11,17 @@ export function ExportNamedDeclaration(node, context) {
// visit children, so bindings are correctly initialised
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') {
// in runes mode, forbid `export let`
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 { mark_subtree_dynamic } from './shared/fragment.js';
import { get_rune } from '../../scope.js';
import { is_component_node } from '../../nodes.js';
/**
* @param {Identifier} node
@ -93,7 +94,10 @@ export function Identifier(node, context) {
context.state.expression.references.add(binding);
context.state.expression.has_state ||=
binding.kind !== 'static' &&
!binding.is_function() &&
(binding.kind === 'prop' ||
binding.kind === 'bindable_prop' ||
binding.kind === 'rest_prop' ||
!binding.is_function()) &&
!context.state.scope.evaluate(node).is_known;
}
@ -152,5 +156,37 @@ export function Identifier(node, context) {
) {
w.reactive_declaration_module_script_dependency(node);
}
if (binding.metadata?.is_template_declaration && context.state.options.experimental.async) {
let snippet_name;
// Find out if this references a {@const ...} declaration of an implicit children snippet
// when it is itself inside a snippet block at the same level. If so, error.
for (let i = context.path.length - 1; i >= 0; i--) {
const parent = context.path[i];
const grand_parent = context.path[i - 1];
if (parent.type === 'SnippetBlock') {
snippet_name = parent.expression.name;
} else if (
snippet_name &&
grand_parent &&
parent.type === 'Fragment' &&
(is_component_node(grand_parent) ||
(grand_parent.type === 'SvelteBoundary' &&
(snippet_name === 'failed' || snippet_name === 'pending')))
) {
if (
is_component_node(grand_parent)
? grand_parent.metadata.scopes.default === binding.scope
: context.state.scopes.get(parent) === binding.scope
) {
e.const_tag_invalid_reference(node, node.name);
} else {
break;
}
}
}
}
}
}

@ -35,11 +35,6 @@ export function SnippetBlock(node, context) {
if (can_hoist) {
const binding = /** @type {Binding} */ (context.state.scope.get(name));
context.state.analysis.module.scope.declarations.set(name, binding);
} else {
const undefined_export = context.state.analysis.undefined_exports.get(name);
if (undefined_export) {
e.snippet_invalid_export(undefined_export);
}
}
node.metadata.can_hoist = can_hoist;

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

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

@ -26,6 +26,7 @@ import { DebugTag } from './visitors/DebugTag.js';
import { EachBlock } from './visitors/EachBlock.js';
import { ExportNamedDeclaration } from './visitors/ExportNamedDeclaration.js';
import { ExpressionStatement } from './visitors/ExpressionStatement.js';
import { ForOfStatement } from './visitors/ForOfStatement.js';
import { Fragment } from './visitors/Fragment.js';
import { FunctionDeclaration } from './visitors/FunctionDeclaration.js';
import { FunctionExpression } from './visitors/FunctionExpression.js';
@ -103,6 +104,7 @@ const visitors = {
EachBlock,
ExportNamedDeclaration,
ExpressionStatement,
ForOfStatement,
Fragment,
FunctionDeclaration,
FunctionExpression,
@ -170,6 +172,7 @@ export function client_component(analysis, options) {
// these are set inside the `Fragment` visitor, and cannot be used until then
init: /** @type {any} */ (null),
consts: /** @type {any} */ (null),
update: /** @type {any} */ (null),
after_update: /** @type {any} */ (null),
template: /** @type {any} */ (null),
@ -209,7 +212,8 @@ export function client_component(analysis, options) {
/** @type {ESTree.Statement[]} */
const store_setup = [];
/** @type {ESTree.Statement} */
let store_init = b.empty;
/** @type {ESTree.VariableDeclaration[]} */
const legacy_reactive_declarations = [];
@ -227,8 +231,9 @@ export function client_component(analysis, options) {
if (binding.kind === 'store_sub') {
if (store_setup.length === 0) {
needs_store_cleanup = true;
store_setup.push(
b.const(b.array_pattern([b.id('$$stores'), b.id('$$cleanup')]), b.call('$.setup_stores'))
store_init = b.const(
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));
let component_block = b.block([
store_init,
...store_setup,
...legacy_reactive_declarations,
...group_binding_declarations,
...state.instance_level_snippets,
.../** @type {ESTree.Statement[]} */ (instance.body),
analysis.runes || !analysis.needs_context
? b.empty
: b.stmt(b.call('$.init', analysis.immutable ? b.true : undefined))
...group_binding_declarations
]);
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 =
dev ||
analysis.needs_context ||
@ -378,32 +407,6 @@ export function client_component(analysis, options) {
analysis.uses_slots ||
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
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
// so that it can effectively clean up the store subscription even after the user effects runs
if (should_inject_context) {
@ -610,8 +607,9 @@ export function client_component(analysis, options) {
);
}
if (analysis.custom_element) {
const ce = analysis.custom_element;
const ce = options.customElementOptions ?? options.customElement;
if (ce) {
const ce_props = typeof ce === 'boolean' ? {} : ce.props || {};
/** @type {ESTree.Property[]} */

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

@ -1,4 +1,4 @@
/** @import { ArrowFunctionExpression, AssignmentExpression, Expression, FunctionDeclaration, FunctionExpression, Identifier, Node, Pattern, UpdateExpression } from 'estree' */
/** @import { ArrowFunctionExpression, AssignmentExpression, BlockStatement, Expression, FunctionDeclaration, FunctionExpression, Identifier, Node, Pattern, UpdateExpression } from 'estree' */
/** @import { Binding } from '#compiler' */
/** @import { ClientTransformState, ComponentClientTransformState, ComponentContext } from './types.js' */
/** @import { Analysis } from '../../types.js' */
@ -289,8 +289,15 @@ export function should_proxy(node, scope) {
/**
* Svelte legacy mode should use safe equals in most places, runes mode shouldn't
* @param {ComponentClientTransformState} state
* @param {Expression} arg
* @param {Expression | BlockStatement} expression
* @param {boolean} [async]
*/
export function create_derived(state, arg) {
return b.call(state.analysis.runes ? '$.derived' : '$.derived_safe_equal', arg);
export function create_derived(state, expression, async = false) {
const thunk = b.thunk(expression, async);
if (async) {
return b.call(b.await(b.call('$.save', b.call('$.async_derived', thunk))));
} else {
return b.call(state.analysis.runes ? '$.derived' : '$.derived_safe_equal', thunk);
}
}

@ -1,7 +1,7 @@
/** @import { BlockStatement, Pattern, Statement } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentClientTransformState, ComponentContext } from '../types' */
import { extract_identifiers } from '../../../../utils/ast.js';
import { extract_identifiers, is_expression_async } from '../../../../utils/ast.js';
import * as b from '#compiler/builders';
import { create_derived } from '../utils.js';
import { get_value } from './shared/declarations.js';
@ -15,7 +15,10 @@ export function AwaitBlock(node, context) {
context.state.template.push_comment();
// Visit {#await <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 catch_block;
@ -93,13 +96,13 @@ function create_derived_block_argument(node, context) {
b.return(b.object(identifiers.map((identifier) => b.prop('init', identifier, identifier))))
]);
const declarations = [b.var(value, create_derived(context.state, b.thunk(block)))];
const declarations = [b.var(value, create_derived(context.state, block))];
for (const id of identifiers) {
context.state.transform[id.name] = { read: get_value };
declarations.push(
b.var(id, create_derived(context.state, b.thunk(b.member(b.call('$.get', value), id))))
b.var(id, create_derived(context.state, b.member(b.call('$.get', value), id)))
);
}

@ -85,7 +85,9 @@ export function CallExpression(node, context) {
['debug', 'dir', 'error', 'group', 'groupCollapsed', 'info', 'log', 'trace', 'warn'].includes(
node.callee.property.name
) &&
node.arguments.some((arg) => arg.type !== 'Literal') // TODO more cases?
node.arguments.some(
(arg) => arg.type === 'SpreadElement' || context.state.scope.evaluate(arg).has_unknown
)
) {
return b.call(
node.callee,

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

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

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

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

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

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

@ -171,11 +171,14 @@ export function VariableDeclaration(node, context) {
context.state.transform[id.name] = { read: get_value };
const expression = /** @type {Expression} */ (context.visit(b.thunk(value)));
const call = b.call('$.derived', expression);
return b.declarator(
id,
dev ? b.call('$.tag', call, b.literal('[$state iterable]')) : call
);
let call = b.call('$.derived', expression);
if (dev) {
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) => {
const value = /** @type {Expression} */ (context.visit(path.expression));
@ -228,19 +231,37 @@ export function VariableDeclaration(node, context) {
}
} else {
const init = /** @type {CallExpression} */ (declarator.init);
let expression = /** @type {Expression} */ (
context.visit(value, {
...context.state,
in_derived: rune === '$derived'
})
);
let rhs = value;
if (rune !== '$derived' || init.arguments[0].type !== 'Identifier') {
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);
let expression = /** @type {Expression} */ (context.visit(value));
if (rune === '$derived') expression = b.thunk(expression);
const call = b.call('$.derived', expression);
declarations.push(
b.declarator(id, dev ? b.call('$.tag', call, b.literal('[$derived iterable]')) : call)
);
if (is_async) {
const location = dev && !is_ignored(init, 'await_waterfall') && locate_node(init);
call = b.call(
'$.async_derived',
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);
@ -250,10 +271,14 @@ export function VariableDeclaration(node, context) {
context.state.transform[id.name] = { read: get_value };
const expression = /** @type {Expression} */ (context.visit(b.thunk(value)));
const call = b.call('$.derived', expression);
declarations.push(
b.declarator(id, dev ? b.call('$.tag', call, b.literal('[$derived iterable]')) : call)
);
let call = b.call('$.derived', expression);
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) {

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

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

@ -23,6 +23,15 @@ export function is_element_node(node) {
return element_nodes.includes(node.type);
}
/**
* Returns true for all component-like nodes
* @param {AST.SvelteNode} node
* @returns {node is AST.Component | AST.SvelteComponent | AST.SvelteSelf}
*/
export function is_component_node(node) {
return ['Component', 'SvelteComponent', 'SvelteSelf'].includes(node.type);
}
/**
* @param {AST.RegularElement | AST.SvelteElement} node
* @returns {boolean}

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

@ -95,7 +95,6 @@ export interface ComponentAnalysis extends Analysis {
};
/** @deprecated use `source` from `state.js` instead */
source: string;
undefined_exports: Map<string, Node>;
/**
* 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 { SourceUpdate, Source } from './private.js' */
/** @import { DecodedSourceMap, RawSourceMap } from '@ampproject/remapping' */
/** @import { DecodedSourceMap, RawSourceMap } from '@jridgewell/remapping' */
import { getLocator } from 'locate-character';
import {
MappedCode,
@ -25,7 +25,7 @@ class PreprocessResult {
// sourcemap_list is sorted in reverse order from last map (index 0) to first map (index -1)
// so we use sourcemap_list.unshift() to add new maps
// https://github.com/ampproject/remapping#multiple-transformations-of-a-file
// https://github.com/jridgewell/sourcemaps/tree/main/packages/remapping#multiple-transformations-of-a-file
/**
* @default []

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

@ -87,7 +87,7 @@ export function pop_ignore() {
/**
* @param {AST.SvelteNode | NodeLike} node
* @param {import('../constants.js').IGNORABLE_RUNTIME_WARNINGS[number]} code
* @param {typeof import('../constants.js').IGNORABLE_RUNTIME_WARNINGS[number]} code
* @returns
*/
export function is_ignored(node, code) {

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

@ -56,15 +56,6 @@ export function assignment(operator, left, right) {
return { type: 'AssignmentExpression', operator, left, right };
}
/**
* @template T
* @param {T & ESTree.BaseFunction} func
* @returns {T & ESTree.BaseFunction}
*/
export function async(func) {
return { ...func, async: true };
}
/**
* @param {ESTree.Expression} argument
* @returns {ESTree.AwaitExpression}
@ -214,6 +205,23 @@ export function export_default(declaration) {
return { type: 'ExportDefaultDeclaration', declaration };
}
/**
* @param {ESTree.VariableDeclaration | ESTree.Pattern} left
* @param {ESTree.Expression} right
* @param {ESTree.Statement} body
* @param {boolean} [_await]
* @returns {ESTree.ForOfStatement}
*/
export function for_of(left, right, body, _await = false) {
return {
type: 'ForOfStatement',
left,
right,
body,
await: _await
};
}
/**
* @param {ESTree.Identifier} id
* @param {ESTree.Pattern[]} params
@ -580,14 +588,14 @@ export function method(kind, key, params, body, computed = false, is_static = fa
* @param {ESTree.BlockStatement} body
* @returns {ESTree.FunctionExpression}
*/
function function_builder(id, params, body) {
function function_builder(id, params, body, async = false) {
return {
type: 'FunctionExpression',
id,
params,
body,
generator: false,
async: false,
async,
metadata: /** @type {any} */ (null) // should not be used by codegen
};
}

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

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

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

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

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

@ -4,7 +4,7 @@ import { snapshot } from '../../shared/clone.js';
import { define_property } from '../../shared/utils.js';
import { DERIVED, ASYNC, PROXY_PATH_SYMBOL, STATE_SYMBOL } from '#client/constants';
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 {{
@ -26,7 +26,7 @@ function log_entry(signal, entry) {
return;
}
const type = (signal.f & (DERIVED | ASYNC)) !== 0 ? '$derived' : '$state';
const type = get_type(signal);
const current_reaction = /** @type {Reaction} */ (active_reaction);
const dirty = signal.wv > current_reaction.wv || current_reaction.wv === 0;
const style = dirty
@ -73,6 +73,15 @@ function log_entry(signal, entry) {
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
* @param {() => string} label

@ -28,6 +28,8 @@ const PENDING = 0;
const THEN = 1;
const CATCH = 2;
/** @typedef {typeof PENDING | typeof THEN | typeof CATCH} AwaitState */
/**
* @template V
* @param {TemplateNode} node
@ -67,9 +69,8 @@ export function await_block(node, get_input, pending_fn, then_fn, catch_fn) {
: mutable_source(/** @type {V} */ (undefined), false, false);
var error_source = runes ? source(undefined) : mutable_source(undefined, false, false);
var resolved = false;
/**
* @param {PENDING | THEN | CATCH} state
* @param {AwaitState} state
* @param {boolean} restore
*/
function update(state, restore) {

@ -148,7 +148,7 @@ export class Boundary {
// need to use hydration boundary comments to report whether
// the pending or main block was rendered for a given
// boundary, and hydrate accordingly
queueMicrotask(() => {
Batch.enqueue(() => {
this.#main_effect = this.#run(() => {
Batch.ensure();
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
each_effect ??= /** @type {Effect} */ (active_effect);
array = get(each_array);
array = /** @type {V[]} */ (get(each_array));
var length = array.length;
if (was_empty && length === 0) {

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

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

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

@ -19,7 +19,7 @@ import { attach } from './attachments.js';
import { clsx } from '../../../shared/attributes.js';
import { set_class } from './class.js';
import { set_style } from './style.js';
import { ATTACHMENT_KEY, NAMESPACE_HTML } from '../../../../constants.js';
import { ATTACHMENT_KEY, NAMESPACE_HTML, UNINITIALIZED } from '../../../../constants.js';
import { block, branch, destroy_effect, effect } from '../../reactivity/effects.js';
import { init_select, select_option } from './bindings/select.js';
import { flatten } from '../../reactivity/async.js';
@ -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,
// because during their upgrade/instantiation they might add more setters.
// 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 ||
customElements.get(node.tagName.toLowerCase())
customElements.get(node.getAttribute('is') || node.tagName.toLowerCase())
? get_setters(node).includes(prop)
: value && typeof value === 'object')
) {
@ -446,6 +446,8 @@ export function set_attributes(element, prev, next, css_hash, skip_warning = fal
) {
// @ts-ignore
element[name] = value;
// remove it from attributes's cache
if (name in attributes) attributes[name] = UNINITIALIZED;
} else if (typeof value !== 'function') {
set_attribute(element, name, value, skip_warning);
}
@ -544,9 +546,10 @@ var setters_cache = new Map();
/** @param {Element} 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;
setters_cache.set(element.nodeName, (setters = []));
setters_cache.set(cache_key, (setters = []));
var descriptors;
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 { queue_micro_task } from '../../task.js';
import { hydrating } from '../../hydration.js';
import { untrack } from '../../../runtime.js';
import { tick, untrack } from '../../../runtime.js';
import { is_runes } from '../../../context.js';
import { current_batch } from '../../../reactivity/batch.js';
import { current_batch, previous_batch } from '../../../reactivity/batch.js';
/**
* @param {HTMLInputElement} input
@ -17,11 +17,9 @@ import { current_batch } from '../../../reactivity/batch.js';
* @returns {void}
*/
export function bind_value(input, get, set = get) {
var runes = is_runes();
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') {
// TODO should this happen in prod too?
e.bind_invalid_checkbox_value();
@ -36,9 +34,13 @@ export function bind_value(input, get, set = get) {
batches.add(current_batch);
}
// In runes mode, respect any validation in accessors (doesn't apply in legacy mode,
// because we use mutable state which ensures the render effect always runs)
if (runes && value !== (value = get())) {
// Because `{#each ...}` blocks work by updating sources inside the flush,
// we need to wait a tick before checking to see if we should forcibly
// update the input and reset the selection state
await tick();
// Respect any validation in accessors
if (value !== (value = get())) {
var start = input.selectionStart;
var end = input.selectionEnd;
@ -76,13 +78,18 @@ export function bind_value(input, get, set = get) {
var value = get();
if (input === document.activeElement && batches.has(/** @type {Batch} */ (current_batch))) {
if (input === document.activeElement) {
// we need both, because in non-async mode, render effects run before previous_batch is set
var batch = /** @type {Batch} */ (previous_batch ?? current_batch);
// Never rewrite the contents of a focused input. We can get here if, for example,
// an update is deferred because of async work depending on the input:
//
// <input bind:value={query}>
// <p>{await find(query)}</p>
return;
if (batches.has(batch)) {
return;
}
}
if (is_numberlike_input(input) && value === to_number(input.value)) {
@ -240,6 +247,7 @@ export function bind_checked(input, get, set = get) {
* @returns {V[]}
*/
function get_binding_group_value(group, __value, checked) {
/** @type {Set<V>} */
var value = new Set();
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}
* @param {Event} event
@ -153,14 +160,19 @@ export function handle_event_propagation(event) {
var path = event.composedPath?.() || [];
var current_target = /** @type {null | Element} */ (path[0] || event.target);
last_propagated_event = event;
// composedPath contains list of nodes the event has propagated through.
// We check __root to skip all nodes below it in case this is a
// parent of the __root node, which indicates that there's nested
// mounted apps. In this case we don't want to trigger events multiple times.
var path_idx = 0;
// the `last_propagated_event === event` check is redundant, but
// without it the variable will be DCE'd and things will
// fail mysteriously in Firefox
// @ts-expect-error is added below
var handled_at = event.__root;
var handled_at = last_propagated_event === event && event.__root;
if (handled_at) {
var at_idx = path.indexOf(handled_at);

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

@ -156,7 +156,7 @@ export function from_mathml(content, flags) {
/**
* @param {TemplateStructure[]} structure
* @param {NAMESPACE_SVG | NAMESPACE_MATHML | undefined} [ns]
* @param {typeof NAMESPACE_SVG | typeof NAMESPACE_MATHML | undefined} [ns]
*/
function fragment_from_tree(structure, ns) {
var fragment = create_fragment();

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

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

@ -11,7 +11,7 @@ import {
set_active_effect,
set_active_reaction
} from '../runtime.js';
import { current_batch } from './batch.js';
import { current_batch, suspend } from './batch.js';
import {
async_derived,
current_async_effect,
@ -19,6 +19,7 @@ import {
derived_safe_equal,
set_from_async_derived
} from './deriveds.js';
import { aborted } from './effects.js';
/**
*
@ -72,11 +73,13 @@ function capture() {
var previous_effect = active_effect;
var previous_reaction = active_reaction;
var previous_component_context = component_context;
var previous_batch = current_batch;
return function restore() {
set_active_effect(previous_effect);
set_active_reaction(previous_reaction);
set_component_context(previous_component_context);
previous_batch?.activate();
if (DEV) {
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() {
set_active_effect(null);
set_active_reaction(null);
set_component_context(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,
RENDER_EFFECT,
ROOT_EFFECT,
USER_EFFECT
USER_EFFECT,
MAYBE_DIRTY
} from '#client/constants';
import { async_mode_flag } from '../../flags/index.js';
import { deferred, define_property } from '../../shared/utils.js';
@ -21,8 +22,7 @@ import {
is_updating_effect,
set_is_updating_effect,
set_signal_status,
update_effect,
write_version
update_effect
} from '../runtime.js';
import * as e from '../errors.js';
import { flush_tasks } from '../dom/task.js';
@ -38,6 +38,13 @@ const batches = new Set();
/** @type {Batch | null} */
export let current_batch = null;
/**
* This is needed to avoid overwriting inputs in non-async mode
* TODO 6.0 remove this, as non-async mode will go away
* @type {Batch | null}
*/
export let previous_batch = null;
/**
* When time travelling, we re-evaluate deriveds based on the temporary
* values of their dependencies rather than their actual values, and cache
@ -49,6 +56,19 @@ export let batch_deriveds = null;
/** @type {Set<() => void>} */
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[]} */
let queued_root_effects = [];
@ -56,6 +76,7 @@ let queued_root_effects = [];
let last_scheduled_effect = null;
let is_flushing = false;
let is_flushing_sync = false;
export class Batch {
/**
@ -63,7 +84,7 @@ export class Batch {
* They keys of this map are identical to `this.#previous`
* @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.
@ -132,6 +153,18 @@ export class Batch {
*/
#block_effects = [];
/**
* Deferred effects (which run after async work has completed) that are DIRTY
* @type {Effect[]}
*/
#dirty_effects = [];
/**
* Deferred effects that are MAYBE_DIRTY
* @type {Effect[]}
*/
#maybe_dirty_effects = [];
/**
* A set of branches that still exist, but will be destroyed when this batch
* is committed we skip over these during `process`
@ -143,20 +176,22 @@ export class Batch {
*
* @param {Effect[]} root_effects
*/
#process(root_effects) {
process(root_effects) {
queued_root_effects = [];
previous_batch = null;
/** @type {Map<Source, { v: unknown, wv: number }> | null} */
var current_values = null;
// if there are multiple batches, we are 'time travelling' —
// we need to undo the changes belonging to any batch
// other than the current one
if (batches.size > 1) {
if (async_mode_flag && batches.size > 1) {
current_values = new Map();
batch_deriveds = new Map();
for (const [source, current] of this.#current) {
for (const [source, current] of this.current) {
current_values.set(source, { v: source.v, wv: source.wv });
source.v = current;
}
@ -180,6 +215,8 @@ export class Batch {
// if we didn't start any new async work, and no async work
// is outstanding from a previous flush, commit
if (this.#async_effects.length === 0 && this.#pending === 0) {
this.#commit();
var render_effects = this.#render_effects;
var effects = this.#effects;
@ -187,17 +224,28 @@ export class Batch {
this.#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(effects);
// Reinstate the current batch if there was no new one created, as `process()` runs in a loop in `flush_effects()`.
// That method expects `current_batch` to be set, and could run the loop again if effects result in new effects
// being scheduled but without writes happening in which case no new batch is created.
if (current_batch === null) {
current_batch = this;
} else {
batches.delete(this);
}
this.#deferred?.resolve();
} else {
// otherwise mark effects clean so they get scheduled on the next run
for (const e of this.#render_effects) set_signal_status(e, CLEAN);
for (const e of this.#effects) set_signal_status(e, CLEAN);
for (const e of this.#block_effects) set_signal_status(e, CLEAN);
this.#defer_effects(this.#render_effects);
this.#defer_effects(this.#effects);
this.#defer_effects(this.#block_effects);
}
if (current_values) {
@ -248,11 +296,11 @@ export class Batch {
this.#effects.push(effect);
} else if (async_mode_flag && (flags & RENDER_EFFECT) !== 0) {
this.#render_effects.push(effect);
} else if (is_dirty(effect)) {
} else if ((flags & CLEAN) === 0) {
if ((flags & ASYNC) !== 0) {
var effects = effect.b?.pending ? this.#boundary_async_effects : this.#async_effects;
effects.push(effect);
} else {
} else if (is_dirty(effect)) {
if ((effect.f & BLOCK_EFFECT) !== 0) this.#block_effects.push(effect);
update_effect(effect);
}
@ -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
* batch, noting its previous and current values
@ -287,7 +350,7 @@ export class Batch {
this.#previous.set(source, value);
}
this.#current.set(source, source.v);
this.current.set(source, source.v);
}
activate() {
@ -296,6 +359,7 @@ export class Batch {
deactivate() {
current_batch = null;
previous_batch = null;
for (const update of effect_pending_updates) {
effect_pending_updates.delete(update);
@ -314,13 +378,13 @@ export class Batch {
flush() {
if (queued_root_effects.length > 0) {
this.flush_effects();
flush_effects();
} else {
this.#commit();
}
if (current_batch !== this) {
// this can happen if a `flushSync` occurred during `this.flush_effects()`,
// this can happen if a `flushSync` occurred during `flush_effects()`,
// which is permitted in legacy mode despite being a terrible idea
return;
}
@ -332,52 +396,6 @@ export class Batch {
this.deactivate();
}
flush_effects() {
var was_updating_effect = is_updating_effect;
is_flushing = true;
try {
var flush_count = 0;
set_is_updating_effect(true);
while (queued_root_effects.length > 0) {
if (flush_count++ > 1000) {
if (DEV) {
var updates = new Map();
for (const source of this.#current.keys()) {
for (const [stack, update] of source.updated ?? []) {
var entry = updates.get(stack);
if (!entry) {
entry = { error: update.error, count: 0 };
updates.set(stack, entry);
}
entry.count += update.count;
}
}
for (const update of updates.values()) {
// eslint-disable-next-line no-console
console.error(update.error);
}
}
infinite_loop_guard();
}
this.#process(queued_root_effects);
old_values.clear();
}
} finally {
is_flushing = false;
set_is_updating_effect(was_updating_effect);
last_scheduled_effect = null;
}
}
/**
* Append and remove branches to/from the DOM
*/
@ -399,18 +417,13 @@ export class Batch {
this.#pending -= 1;
if (this.#pending === 0) {
for (const e of this.#render_effects) {
for (const e of this.#dirty_effects) {
set_signal_status(e, DIRTY);
schedule_effect(e);
}
for (const e of this.#effects) {
set_signal_status(e, DIRTY);
schedule_effect(e);
}
for (const e of this.#block_effects) {
set_signal_status(e, DIRTY);
for (const e of this.#maybe_dirty_effects) {
set_signal_status(e, MAYBE_DIRTY);
schedule_effect(e);
}
@ -432,13 +445,13 @@ export class Batch {
return (this.#deferred ??= deferred()).promise;
}
static ensure(autoflush = true) {
static ensure() {
if (current_batch === null) {
const batch = (current_batch = new Batch());
batches.add(current_batch);
if (autoflush) {
queueMicrotask(() => {
if (!is_flushing_sync) {
Batch.enqueue(() => {
if (current_batch !== batch) {
// a flushSync happened in the meantime
return;
@ -451,6 +464,15 @@ export class 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) {
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();
}
var result;
var was_flushing_sync = is_flushing_sync;
is_flushing_sync = true;
const batch = Batch.ensure(false);
try {
var result;
if (fn) {
batch.flush_effects();
if (fn) {
flush_effects();
result = fn();
}
result = fn();
}
while (true) {
flush_tasks();
while (true) {
flush_tasks();
if (queued_root_effects.length === 0) {
current_batch?.flush();
if (queued_root_effects.length === 0) {
if (batch === current_batch) {
batch.flush();
// we need to check again, in case we just updated an `$effect.pending()`
if (queued_root_effects.length === 0) {
// this would be reset in `flush_effects()` but since we are early returning here,
// we need to reset it here as well in case the first time there's 0 queued root effects
last_scheduled_effect = null;
return /** @type {T} */ (result);
}
}
// this would be reset in `batch.flush_effects()` but since we are early returning here,
// we need to reset it here as well in case the first time there's 0 queued root effects
last_scheduled_effect = null;
flush_effects();
}
} finally {
is_flushing_sync = was_flushing_sync;
}
}
function flush_effects() {
var was_updating_effect = is_updating_effect;
is_flushing = true;
try {
var flush_count = 0;
set_is_updating_effect(true);
while (queued_root_effects.length > 0) {
var batch = Batch.ensure();
if (flush_count++ > 1000) {
if (DEV) {
var updates = new Map();
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
* @returns {void}
@ -517,44 +596,49 @@ function flush_queued_effects(effects) {
var length = effects.length;
if (length === 0) return;
for (var i = 0; i < length; i++) {
var effect = effects[i];
if ((effect.f & (DESTROYED | INERT)) === 0) {
if (is_dirty(effect)) {
var wv = write_version;
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
// don't know if we need to keep them until they are executed. Doing the check
// here (rather than in `update_effect`) allows us to skip the work for
// immediate effects.
if (effect.deps === null && effect.first === null && effect.nodes_start === null) {
// if there's no teardown or abort controller we completely unlink
// the effect from the graph
if (effect.teardown === null && effect.ac === null) {
// remove this effect from the graph
unlink_effect(effect);
} else {
// keep the effect in the graph, but free up some memory
effect.fn = null;
}
var i = 0;
while (i < length) {
var effect = effects[i++];
if ((effect.f & (DESTROYED | INERT)) === 0 && is_dirty(effect)) {
eager_block_effects = [];
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
// don't know if we need to keep them until they are executed. Doing the check
// here (rather than in `update_effect`) allows us to skip the work for
// immediate effects.
if (effect.deps === null && effect.first === null && effect.nodes_start === null) {
// if there's no teardown or abort controller we completely unlink
// the effect from the graph
if (effect.teardown === null && effect.ac === null) {
// remove this effect from the graph
unlink_effect(effect);
} else {
// 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
// effects that should be removed as a result of the state change
if (write_version > wv && (effect.f & USER_EFFECT) !== 0) {
break;
for (const e of eager_block_effects) {
update_effect(e);
}
eager_block_effects = [];
}
}
}
for (; i < length; i += 1) {
schedule_effect(effects[i]);
}
eager_block_effects = null;
}
/**
@ -593,7 +677,13 @@ export function suspend() {
return function unsuspend() {
boundary.update_pending_count(-1);
if (!pending) batch.decrement();
if (!pending) {
batch.activate();
batch.decrement();
} else {
batch.deactivate();
}
unset_context();
};

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

@ -1,13 +1,11 @@
/** @import { ComponentContext } from '#client' */
/** @import { Derived, Effect, Source } from './types.js' */
/** @import { Effect, Source } from './types.js' */
import { DEV } from 'esm-env';
import {
PROPS_IS_BINDABLE,
PROPS_IS_IMMUTABLE,
PROPS_IS_LAZY_INITIAL,
PROPS_IS_RUNES,
PROPS_IS_UPDATED,
UNINITIALIZED
PROPS_IS_UPDATED
} from '../../../constants.js';
import { get_descriptor, is_function } from '../../shared/utils.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
* that looks like `() => { dynamic: props }, { static: prop }, ..` and wraps
* them so that the whole thing is passed to the component as the `$$props` argument.
* @template {Record<string | symbol, unknown>} T
* @type {ProxyHandler<{ props: Array<T | (() => T)> }>}}
* @type {ProxyHandler<{ props: Array<Record<string | symbol, unknown> | (() => Record<string | symbol, unknown>)> }>}}
*/
const spread_props_handler = {
get(target, key) {
@ -364,22 +361,23 @@ export function prop(props, key, flags, fallback) {
// means we can just call `$$props.foo = value` directly
if (setter) {
var legacy_parent = props.$$legacy;
return function (/** @type {any} */ value, /** @type {boolean} */ mutation) {
if (arguments.length > 0) {
// We don't want to notify if the value was mutated and the parent is in runes mode.
// In that case the state proxy (if it exists) should take care of the notification.
// If the parent is not in runes mode, we need to notify on mutation, too, that the prop
// has changed because the parent will not be able to detect the change otherwise.
if (!runes || !mutation || legacy_parent || is_store_sub) {
/** @type {Function} */ (setter)(mutation ? getter() : value);
return /** @type {() => V} */ (
function (/** @type {V} */ value, /** @type {boolean} */ mutation) {
if (arguments.length > 0) {
// We don't want to notify if the value was mutated and the parent is in runes mode.
// In that case the state proxy (if it exists) should take care of the notification.
// If the parent is not in runes mode, we need to notify on mutation, too, that the prop
// has changed because the parent will not be able to detect the change otherwise.
if (!runes || !mutation || legacy_parent || is_store_sub) {
/** @type {Function} */ (setter)(mutation ? getter() : value);
}
return value;
}
return value;
return getter();
}
return getter();
};
);
}
// Either prop is written to, but there's no binding, which means we
@ -393,34 +391,40 @@ export function prop(props, key, flags, fallback) {
return getter();
});
if (DEV) {
d.label = key;
}
// Capture the initial value if it's bindable
if (bindable) get(d);
var parent_effect = /** @type {Effect} */ (active_effect);
return function (/** @type {any} */ value, /** @type {boolean} */ mutation) {
if (arguments.length > 0) {
const new_value = mutation ? get(d) : runes && bindable ? proxy(value) : value;
return /** @type {() => V} */ (
function (/** @type {any} */ value, /** @type {boolean} */ mutation) {
if (arguments.length > 0) {
const new_value = mutation ? get(d) : runes && bindable ? proxy(value) : value;
set(d, new_value);
overridden = true;
set(d, new_value);
overridden = true;
if (fallback_value !== undefined) {
fallback_value = new_value;
if (fallback_value !== undefined) {
fallback_value = new_value;
}
return value;
}
return value;
}
// special case — avoid recalculating the derived if we're in a
// teardown function and the prop was overridden locally, or the
// component was already destroyed (this latter part is necessary
// because `bind:this` can read props after the component has
// been destroyed. TODO simplify `bind:this`
if ((is_destroying_effect && overridden) || (parent_effect.f & DESTROYED) !== 0) {
return d.v;
}
// special case — avoid recalculating the derived if we're in a
// teardown function and the prop was overridden locally, or the
// component was already destroyed (this latter part is necessary
// because `bind:this` can read props after the component has
// been destroyed. TODO simplify `bind:this`
if ((is_destroying_effect && overridden) || (parent_effect.f & DESTROYED) !== 0) {
return d.v;
return get(d);
}
return get(d);
};
);
}

@ -33,10 +33,11 @@ import * as e from '../errors.js';
import { legacy_mode_flag, tracing_mode_flag } from '../../flags/index.js';
import { get_stack, tag_proxy } from '../dev/tracing.js';
import { component_context, is_runes } from '../context.js';
import { Batch, schedule_effect } from './batch.js';
import { Batch, eager_block_effects, schedule_effect } from './batch.js';
import { proxy } from '../proxy.js';
import { execute_derived } from './deriveds.js';
/** @type {Set<any>} */
export let inspect_effects = new Set();
/** @type {Map<Source, any>} */
@ -179,7 +180,7 @@ export function internal_set(source, value) {
source.v = value;
const batch = Batch.ensure();
var batch = Batch.ensure();
batch.capture(source, old_value);
if (DEV) {
@ -314,9 +315,6 @@ function mark_reactions(signal, status) {
var reaction = reactions[i];
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
if (!runes && reaction === active_effect) continue;
@ -326,15 +324,23 @@ function mark_reactions(signal, status) {
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
if ((flags & (CLEAN | UNOWNED)) !== 0) {
if ((flags & DERIVED) !== 0) {
mark_reactions(/** @type {Derived} */ (reaction), MAYBE_DIRTY);
} else {
schedule_effect(/** @type {Effect} */ (reaction));
// don't set a DIRTY reaction to MAYBE_DIRTY
if (not_dirty) {
set_signal_status(reaction, status);
}
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 { teardown } from './effects.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
@ -33,6 +34,10 @@ export function store_get(store, store_name, stores) {
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 (entry.store !== store && !(IS_UNMOUNTED in stores)) {
entry.unsubscribe();

@ -54,7 +54,7 @@ export interface Reaction extends Signal {
export interface Derived<V = unknown> extends Value<V>, Reaction {
/** The derived function */
fn: () => V;
/** Effects created inside this signal */
/** Effects created inside this signal. Used to destroy those effects when the derived reruns or is cleaned up */
effects: null | Effect[];
/** Parent effect or derived */
parent: Effect | Derived | null;

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

@ -22,7 +22,7 @@ import {
STALE_REACTION,
ERROR_VALUE
} from './constants.js';
import { internal_set, old_values } from './reactivity/sources.js';
import { old_values } from './reactivity/sources.js';
import {
destroy_derived_effects,
execute_derived,
@ -45,6 +45,8 @@ import * as w from './warnings.js';
import { Batch, batch_deriveds, flushSync, schedule_effect } from './reactivity/batch.js';
import { handle_error } from './error-handling.js';
import { UNINITIALIZED } from '../../constants.js';
import { captured_signals } from './legacy.js';
import { without_reactive_context } from './dom/elements/bindings/shared.js';
export let is_updating_effect = false;
@ -137,14 +139,6 @@ export function set_update_version(value) {
// If we are working with a get() chain that has no active container,
// to prevent memory leaks, we skip adding the reaction.
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() {
return ++write_version;
@ -285,13 +279,17 @@ export function update_reaction(reaction) {
update_version = ++read_version;
if (reaction.ac !== null) {
reaction.ac.abort(STALE_REACTION);
without_reactive_context(() => {
/** @type {AbortController} */ (reaction.ac).abort(STALE_REACTION);
});
reaction.ac = null;
}
try {
reaction.f |= REACTION_IS_UPDATING;
var result = /** @type {Function} */ (0, reaction.fn)();
var fn = /** @type {Function} */ (reaction.fn);
var result = fn();
var deps = reaction.deps;
if (new_deps !== null) {
@ -531,9 +529,7 @@ export function get(signal) {
var flags = signal.f;
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.
if (active_reaction !== null && !untracking) {
@ -713,45 +709,6 @@ export function safe_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),
* any state read inside `fn` will not be treated as a dependency.

@ -6,7 +6,12 @@ export class HeadPayload {
uid = () => '';
title = '';
constructor(css = new Set(), /** @type {string[]} */ out = [], title = '', uid = () => '') {
constructor(
/** @type {Set<{ hash: string; code: string }>} */ css = new Set(),
/** @type {string[]} */ out = [],
title = '',
uid = () => ''
) {
this.css = css;
this.out = out;
this.title = title;

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

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

@ -82,6 +82,10 @@ export function createSubscriber(start) {
if (subscribers === 0) {
stop?.();
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