diff --git a/.changeset/fuzzy-zoos-repeat.md b/.changeset/fuzzy-zoos-repeat.md
new file mode 100644
index 0000000000..3fb3f0502e
--- /dev/null
+++ b/.changeset/fuzzy-zoos-repeat.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+fix: value/checked not correctly set using spread
diff --git a/.changeset/hip-singers-vanish.md b/.changeset/hip-singers-vanish.md
new file mode 100644
index 0000000000..9dce4d98a8
--- /dev/null
+++ b/.changeset/hip-singers-vanish.md
@@ -0,0 +1,5 @@
+---
+'svelte': minor
+---
+
+feat: SSR-safe ID generation with `$props.id()`
diff --git a/.changeset/slow-meals-wait.md b/.changeset/slow-meals-wait.md
new file mode 100644
index 0000000000..e1408e3849
--- /dev/null
+++ b/.changeset/slow-meals-wait.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+fix: use `importNode` to clone templates for Firefox
diff --git a/.changeset/thick-carrots-arrive.md b/.changeset/thick-carrots-arrive.md
new file mode 100644
index 0000000000..582cf5e6e1
--- /dev/null
+++ b/.changeset/thick-carrots-arrive.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+fix: recurse into `$derived` for ownership validation
diff --git a/.github/workflows/docs-preview-create-request.yml b/.github/workflows/docs-preview-create-request.yml
deleted file mode 100644
index f57766dc36..0000000000
--- a/.github/workflows/docs-preview-create-request.yml
+++ /dev/null
@@ -1,26 +0,0 @@
-# https://github.com/sveltejs/svelte.dev/blob/main/apps/svelte.dev/scripts/sync-docs/README.md
-name: Docs preview create request
-
-on:
- pull_request_target:
- branches:
- - main
-
-jobs:
- dispatch:
- runs-on: ubuntu-latest
- steps:
- - name: Repository Dispatch
- uses: peter-evans/repository-dispatch@v3
- with:
- token: ${{ secrets.SYNC_REQUEST_TOKEN }}
- repository: sveltejs/svelte.dev
- event-type: docs-preview-create
- client-payload: |-
- {
- "package": "svelte",
- "repo": "${{ github.repository }}",
- "owner": "${{ github.event.pull_request.head.repo.owner.login }}",
- "branch": "${{ github.event.pull_request.head.ref }}",
- "pr": ${{ github.event.pull_request.number }}
- }
diff --git a/.github/workflows/docs-preview-delete-request.yml b/.github/workflows/docs-preview-delete-request.yml
deleted file mode 100644
index 4eb0e996a6..0000000000
--- a/.github/workflows/docs-preview-delete-request.yml
+++ /dev/null
@@ -1,27 +0,0 @@
-# https://github.com/sveltejs/svelte.dev/blob/main/apps/svelte.dev/scripts/sync-docs/README.md
-name: Docs preview delete request
-
-on:
- pull_request_target:
- branches:
- - main
- types: [closed]
-
-jobs:
- dispatch:
- runs-on: ubuntu-latest
- steps:
- - name: Repository Dispatch
- uses: peter-evans/repository-dispatch@v3
- with:
- token: ${{ secrets.SYNC_REQUEST_TOKEN }}
- repository: sveltejs/svelte.dev
- event-type: docs-preview-delete
- client-payload: |-
- {
- "package": "svelte",
- "repo": "${{ github.repository }}",
- "owner": "${{ github.event.pull_request.head.repo.owner.login }}",
- "branch": "${{ github.event.pull_request.head.ref }}",
- "pr": ${{ github.event.pull_request.number }}
- }
diff --git a/.github/workflows/pkg.pr.new.yml b/.github/workflows/pkg.pr.new.yml
index 4292ec900a..99f8153517 100644
--- a/.github/workflows/pkg.pr.new.yml
+++ b/.github/workflows/pkg.pr.new.yml
@@ -9,6 +9,9 @@ jobs:
- name: Checkout code
uses: actions/checkout@v4
+ - name: install corepack
+ run: npm i -g corepack@0.31.0
+
- run: corepack enable
- uses: actions/setup-node@v4
with:
diff --git a/.github/workflows/sync-request.yml b/.github/workflows/sync-request.yml
deleted file mode 100644
index de2ce77692..0000000000
--- a/.github/workflows/sync-request.yml
+++ /dev/null
@@ -1,22 +0,0 @@
-# https://github.com/sveltejs/svelte.dev/blob/main/apps/svelte.dev/scripts/sync-docs/README.md
-name: Sync request
-
-on:
- push:
- branches:
- - main
-
-jobs:
- dispatch:
- runs-on: ubuntu-latest
- steps:
- - name: Repository Dispatch
- uses: peter-evans/repository-dispatch@v3
- with:
- token: ${{ secrets.SYNC_REQUEST_TOKEN }}
- repository: sveltejs/svelte.dev
- event-type: sync-request
- client-payload: |-
- {
- "package": "svelte"
- }
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index f7d15f905e..0e2628f84f 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -51,7 +51,7 @@ We use [GitHub issues](https://github.com/sveltejs/svelte/issues) for our public
If you have questions about using Svelte, contact us on Discord at [svelte.dev/chat](https://svelte.dev/chat), and we will do our best to answer your questions.
-If you see anything you'd like to be implemented, create a [feature request issue](https://github.com/sveltejs/svelte/issues/new?template=feature_request.yml)
+If you see anything you'd like to be implemented, create a [feature request issue](https://github.com/sveltejs/svelte/issues/new?template=feature_request.yml).
### Reporting new issues
diff --git a/documentation/docs/02-runes/04-$effect.md b/documentation/docs/02-runes/04-$effect.md
index 1ea960de70..da24084d4d 100644
--- a/documentation/docs/02-runes/04-$effect.md
+++ b/documentation/docs/02-runes/04-$effect.md
@@ -66,7 +66,7 @@ You can return a function from `$effect`, which will run immediately before the
### Understanding dependencies
-`$effect` automatically picks up any reactive values (`$state`, `$derived`, `$props`) that are _synchronously_ read inside its function body and registers them as dependencies. When those dependencies change, the `$effect` schedules a rerun.
+`$effect` automatically picks up any reactive values (`$state`, `$derived`, `$props`) that are _synchronously_ read inside its function body (including indirectly, via function calls) and registers them as dependencies. When those dependencies change, the `$effect` schedules a rerun.
Values that are read _asynchronously_ — after an `await` or inside a `setTimeout`, for example — will not be tracked. Here, the canvas will be repainted when `color` changes, but not when `size` changes ([demo](/playground/untitled#H4sIAAAAAAAAE31T246bMBD9lZF3pWSlBEirfaEQqdo_2PatVIpjBrDkGGQPJGnEv1e2IZfVal-wfHzmzJyZ4cIqqdCy9M-F0blDlnqArZjmB3f72XWRHVCRw_bc4me4aDWhJstSlllhZEfbQhekkMDKfwg5PFvihMvX5OXH_CJa1Zrb0-Kpqr5jkiwC48rieuDWQbqgZ6wqFLRcvkC-hYvnkWi1dWqa8ESQTxFRjfQWsOXiWzmr0sSLhEJu3p1YsoJkNUcdZUnN9dagrBu6FVRQHAM10sJRKgUG16bXcGxQ44AGdt7SDkTDdY02iqLHnJVU6hedlWuIp94JW6Tf8oBt_8GdTxlF0b4n0C35ZLBzXb3mmYn3ae6cOW74zj0YVzDNYXRHFt9mprNgHfZSl6mzml8CMoLvTV6wTZIUDEJv5us2iwMtiJRyAKG4tXnhl8O0yhbML0Wm-B7VNlSSSd31BG7z8oIZZ6dgIffAVY_5xdU9Qrz1Bnx8fCfwtZ7v8Qc9j3nB8PqgmMWlHIID6-bkVaPZwDySfWtKNGtquxQ23Qlsq2QJT0KIqb8dL0up6xQ2eIBkAg_c1FI_YqW0neLnFCqFpwmreedJYT7XX8FVOBfwWRhXstZrSXiwKQjUhOZeMIleb5JZfHWn2Yq5pWEpmR7Hv-N_wEqT8hEEAAA=)):
diff --git a/documentation/docs/02-runes/05-$props.md b/documentation/docs/02-runes/05-$props.md
index 4b1775bf5a..f300fb239d 100644
--- a/documentation/docs/02-runes/05-$props.md
+++ b/documentation/docs/02-runes/05-$props.md
@@ -199,3 +199,24 @@ You can, of course, separate the type declaration from the annotation:
> [!NOTE] Interfaces for native DOM elements are provided in the `svelte/elements` module (see [Typing wrapper components](typescript#Typing-wrapper-components))
Adding types is recommended, as it ensures that people using your component can easily discover which props they should provide.
+
+
+## `$props.id()`
+
+This rune, added in version 5.20.0, generates an ID that is unique to the current component instance. When hydrating a server-rendered component, the value will be consistent between server and client.
+
+This is useful for linking elements via attributes like `for` and `aria-labelledby`.
+
+```svelte
+
+
+
+```
\ No newline at end of file
diff --git a/documentation/docs/03-template-syntax/09-@const.md b/documentation/docs/03-template-syntax/09-@const.md
index c42d3560fd..2a587b7a3d 100644
--- a/documentation/docs/03-template-syntax/09-@const.md
+++ b/documentation/docs/03-template-syntax/09-@const.md
@@ -11,4 +11,4 @@ The `{@const ...}` tag defines a local constant.
{/each}
```
-`{@const}` is only allowed as an immediate child of a block — `{#if ...}`, `{#each ...}`, `{#snippet ...}` and so on — a ` ` or a ` ` or a ``.
diff --git a/documentation/docs/03-template-syntax/11-bind.md b/documentation/docs/03-template-syntax/11-bind.md
index 90046c8c45..119f87ed8e 100644
--- a/documentation/docs/03-template-syntax/11-bind.md
+++ b/documentation/docs/03-template-syntax/11-bind.md
@@ -219,11 +219,10 @@ You can give the `` a default value by adding a `selected` attribute to
- [`volume`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/volume)
- [`muted`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/muted)
-...and seven readonly ones:
+...and six readonly ones:
- [`duration`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/duration)
- [`buffered`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/buffered)
-- [`paused`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/paused)
- [`seekable`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/seekable)
- [`seeking`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/seeking_event)
- [`ended`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/ended)
@@ -235,7 +234,7 @@ You can give the `` a default value by adding a `selected` attribute to
## ``
-`` elements have all the same bindings as [#audio] elements, plus readonly [`videoWidth`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLVideoElement/videoWidth) and [`videoHeight`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLVideoElement/videoHeight) bindings.
+`` elements have all the same bindings as [``](#audio) elements, plus readonly [`videoWidth`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLVideoElement/videoWidth) and [`videoHeight`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLVideoElement/videoHeight) bindings.
## ` `
diff --git a/documentation/docs/04-styling/01-scoped-styles.md b/documentation/docs/04-styling/01-scoped-styles.md
index f870d0a5b8..eae26d0cb1 100644
--- a/documentation/docs/04-styling/01-scoped-styles.md
+++ b/documentation/docs/04-styling/01-scoped-styles.md
@@ -33,10 +33,7 @@ If a component defines `@keyframes`, the name is scoped to the component using t
/* these keyframes are only accessible inside this component */
@keyframes bounce {
- /* ... *.
+ /* ... */
}
```
-
-
-
diff --git a/documentation/docs/05-special-elements/01-svelte-boundary.md b/documentation/docs/05-special-elements/01-svelte-boundary.md
index 15f249a771..f5439b4b83 100644
--- a/documentation/docs/05-special-elements/01-svelte-boundary.md
+++ b/documentation/docs/05-special-elements/01-svelte-boundary.md
@@ -13,7 +13,7 @@ Boundaries allow you to guard against errors in part of your app from breaking t
If an error occurs while rendering or updating the children of a ``, or running any [`$effect`]($effect) functions contained therein, the contents will be removed.
-Errors occurring outside the rendering process (for example, in event handlers) are _not_ caught by error boundaries.
+Errors occurring outside the rendering process (for example, in event handlers or after a `setTimeout` or async work) are _not_ caught by error boundaries.
## Properties
diff --git a/documentation/docs/06-runtime/03-lifecycle-hooks.md b/documentation/docs/06-runtime/03-lifecycle-hooks.md
index a3dbe04b00..2b97ca796f 100644
--- a/documentation/docs/06-runtime/03-lifecycle-hooks.md
+++ b/documentation/docs/06-runtime/03-lifecycle-hooks.md
@@ -45,8 +45,6 @@ If a function is returned from `onMount`, it will be called when the component i
## `onDestroy`
-> EXPORT_SNIPPET: svelte#onDestroy
-
Schedules a callback to run immediately before the component is unmounted.
Out of `onMount`, `beforeUpdate`, `afterUpdate` and `onDestroy`, this is the only one that runs inside a server-side component.
diff --git a/documentation/docs/07-misc/07-v5-migration-guide.md b/documentation/docs/07-misc/07-v5-migration-guide.md
index ce95bf6ac7..94ade6e887 100644
--- a/documentation/docs/07-misc/07-v5-migration-guide.md
+++ b/documentation/docs/07-misc/07-v5-migration-guide.md
@@ -722,7 +722,39 @@ If a bindable property has a default value (e.g. `let { foo = $bindable('bar') }
### `accessors` option is ignored
-Setting the `accessors` option to `true` makes properties of a component directly accessible on the component instance. In runes mode, properties are never accessible on the component instance. You can use component exports instead if you need to expose them.
+Setting the `accessors` option to `true` makes properties of a component directly accessible on the component instance.
+
+```svelte
+
+
+
+```
+
+In runes mode, properties are never accessible on the component instance. You can use component exports instead if you need to expose them.
+
+```svelte
+
+```
+
+Alternatively, if the place where they are instantiated is under your control, you can also make use of runes inside `.js/.ts` files by adjusting their ending to include `.svelte`, i.e. `.svelte.js` or `.svelte.ts`, and then use `$state`:
+
+```js
++++import { mount } from 'svelte';+++
+import App from './App.svelte'
+
+---const app = new App({ target: document.getElementById("app"), props: { foo: 'bar' } });
+app.foo = 'baz'---
++++const props = $state({ foo: 'bar' });
+const app = mount(App, { target: document.getElementById("app"), props });
+props.foo = 'baz';+++
+```
### `immutable` option is ignored
diff --git a/documentation/docs/98-reference/.generated/compile-errors.md b/documentation/docs/98-reference/.generated/compile-errors.md
index 58aabe20cb..7eb53cff93 100644
--- a/documentation/docs/98-reference/.generated/compile-errors.md
+++ b/documentation/docs/98-reference/.generated/compile-errors.md
@@ -573,7 +573,13 @@ Unrecognised compiler option %keypath%
### props_duplicate
```
-Cannot use `$props()` more than once
+Cannot use `%rune%()` more than once
+```
+
+### props_id_invalid_placement
+
+```
+`$props.id()` can only be used at the top level of components as a variable declaration initializer
```
### props_illegal_name
diff --git a/package.json b/package.json
index 57dc4cdebe..2fe545b361 100644
--- a/package.json
+++ b/package.json
@@ -7,7 +7,7 @@
"license": "MIT",
"packageManager": "pnpm@9.4.0",
"engines": {
- "pnpm": "^9.0.0"
+ "pnpm": ">=9.0.0"
},
"repository": {
"type": "git",
@@ -44,6 +44,6 @@
"typescript": "^5.5.4",
"typescript-eslint": "^8.2.0",
"v8-natives": "^1.2.5",
- "vitest": "^2.0.5"
+ "vitest": "^2.1.9"
}
}
diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md
index d16bdbc327..e112bf6209 100644
--- a/packages/svelte/CHANGELOG.md
+++ b/packages/svelte/CHANGELOG.md
@@ -1,5 +1,97 @@
# svelte
+## 5.19.10
+
+### Patch Changes
+
+- fix: when re-connecting unowned deriveds, remove their unowned flag ([#15255](https://github.com/sveltejs/svelte/pull/15255))
+
+- fix: allow mutation of private derived state ([#15228](https://github.com/sveltejs/svelte/pull/15228))
+
+## 5.19.9
+
+### Patch Changes
+
+- fix: ensure unowned derived dependencies are not duplicated when reactions are skipped ([#15232](https://github.com/sveltejs/svelte/pull/15232))
+
+- fix: hydrate `href` that is part of spread attributes ([#15226](https://github.com/sveltejs/svelte/pull/15226))
+
+## 5.19.8
+
+### Patch Changes
+
+- fix: properly set `value` property of custom elements ([#15206](https://github.com/sveltejs/svelte/pull/15206))
+
+- fix: ensure custom element updates don't run in hydration mode ([#15217](https://github.com/sveltejs/svelte/pull/15217))
+
+- fix: ensure tracking returns true, even if in unowned ([#15214](https://github.com/sveltejs/svelte/pull/15214))
+
+## 5.19.7
+
+### Patch Changes
+
+- chore: remove unused code from signal logic ([#15195](https://github.com/sveltejs/svelte/pull/15195))
+
+- fix: encounter svelte:element in blocks as sibling during pruning css ([#15165](https://github.com/sveltejs/svelte/pull/15165))
+
+## 5.19.6
+
+### Patch Changes
+
+- fix: do not prune selectors like `:global(.foo):has(.scoped)` ([#15140](https://github.com/sveltejs/svelte/pull/15140))
+
+- fix: don't error on slot prop inside block inside other component ([#15148](https://github.com/sveltejs/svelte/pull/15148))
+
+- fix: ensure reactions are correctly attached for unowned deriveds ([#15158](https://github.com/sveltejs/svelte/pull/15158))
+
+- fix: silence a11y attribute warnings when spread attributes present ([#15150](https://github.com/sveltejs/svelte/pull/15150))
+
+- fix: prevent false-positive ownership validations due to hot reload ([#15154](https://github.com/sveltejs/svelte/pull/15154))
+
+- fix: widen ownership when calling setContext ([#15153](https://github.com/sveltejs/svelte/pull/15153))
+
+## 5.19.5
+
+### Patch Changes
+
+- fix: improve derived connection to ownership graph ([#15137](https://github.com/sveltejs/svelte/pull/15137))
+
+- fix: correctly look for sibling elements inside blocks and components when pruning CSS ([#15106](https://github.com/sveltejs/svelte/pull/15106))
+
+## 5.19.4
+
+### Patch Changes
+
+- fix: Add `bind:focused` property to `HTMLAttributes` type ([#15122](https://github.com/sveltejs/svelte/pull/15122))
+
+- fix: lazily connect derievds (in deriveds) to their parent ([#15129](https://github.com/sveltejs/svelte/pull/15129))
+
+- fix: disallow $state/$derived in const tags ([#15115](https://github.com/sveltejs/svelte/pull/15115))
+
+## 5.19.3
+
+### Patch Changes
+
+- fix: don't throw for `undefined` non delegated event handlers ([#15087](https://github.com/sveltejs/svelte/pull/15087))
+
+- fix: consistently set value to blank string when value attribute is undefined ([#15057](https://github.com/sveltejs/svelte/pull/15057))
+
+- fix: optimise || expressions in template ([#15092](https://github.com/sveltejs/svelte/pull/15092))
+
+- fix: correctly handle `novalidate` attribute casing ([#15083](https://github.com/sveltejs/svelte/pull/15083))
+
+- fix: expand boolean attribute support ([#15095](https://github.com/sveltejs/svelte/pull/15095))
+
+- fix: avoid double deriveds in component props ([#15089](https://github.com/sveltejs/svelte/pull/15089))
+
+- fix: add check for `is` attribute to correctly detect custom elements ([#15086](https://github.com/sveltejs/svelte/pull/15086))
+
+## 5.19.2
+
+### Patch Changes
+
+- fix: address regression with untrack ([#15079](https://github.com/sveltejs/svelte/pull/15079))
+
## 5.19.1
### Patch Changes
diff --git a/packages/svelte/elements.d.ts b/packages/svelte/elements.d.ts
index 96f1589800..6d256b5620 100644
--- a/packages/svelte/elements.d.ts
+++ b/packages/svelte/elements.d.ts
@@ -839,6 +839,7 @@ export interface HTMLAttributes extends AriaAttributes, D
readonly 'bind:contentBoxSize'?: Array | undefined | null;
readonly 'bind:borderBoxSize'?: Array | undefined | null;
readonly 'bind:devicePixelContentBoxSize'?: Array | undefined | null;
+ readonly 'bind:focused'?: boolean | undefined | null;
// SvelteKit
'data-sveltekit-keepfocus'?: true | '' | 'off' | undefined | null;
diff --git a/packages/svelte/messages/compile-errors/script.md b/packages/svelte/messages/compile-errors/script.md
index 7e56490a2e..00e3797448 100644
--- a/packages/svelte/messages/compile-errors/script.md
+++ b/packages/svelte/messages/compile-errors/script.md
@@ -120,7 +120,11 @@ This turned out to be buggy and unpredictable, particularly when working with de
## props_duplicate
-> Cannot use `$props()` more than once
+> Cannot use `%rune%()` more than once
+
+## props_id_invalid_placement
+
+> `$props.id()` can only be used at the top level of components as a variable declaration initializer
## props_illegal_name
diff --git a/packages/svelte/package.json b/packages/svelte/package.json
index b725e52fed..a4594b2a5c 100644
--- a/packages/svelte/package.json
+++ b/packages/svelte/package.json
@@ -2,7 +2,7 @@
"name": "svelte",
"description": "Cybernetically enhanced web apps",
"license": "MIT",
- "version": "5.19.1",
+ "version": "5.19.10",
"type": "module",
"types": "./types/index.d.ts",
"engines": {
@@ -143,7 +143,7 @@
"source-map": "^0.7.4",
"tiny-glob": "^0.2.9",
"typescript": "^5.5.4",
- "vitest": "^2.0.5"
+ "vitest": "^2.1.9"
},
"dependencies": {
"@ampproject/remapping": "^2.3.0",
diff --git a/packages/svelte/src/ambient.d.ts b/packages/svelte/src/ambient.d.ts
index 0d6d45dfc2..2e402b4d64 100644
--- a/packages/svelte/src/ambient.d.ts
+++ b/packages/svelte/src/ambient.d.ts
@@ -349,6 +349,15 @@ declare namespace $effect {
declare function $props(): any;
declare namespace $props {
+ /**
+ * Generates an ID that is unique to the current component instance. When hydrating a server-rendered component,
+ * the value will be consistent between server and client.
+ *
+ * This is useful for linking elements via attributes like `for` and `aria-labelledby`.
+ * @since 5.20.0
+ */
+ export function id(): string;
+
// prevent intellisense from being unhelpful
/** @deprecated */
export const apply: never;
diff --git a/packages/svelte/src/compiler/errors.js b/packages/svelte/src/compiler/errors.js
index e0d1189e29..634faf15a2 100644
--- a/packages/svelte/src/compiler/errors.js
+++ b/packages/svelte/src/compiler/errors.js
@@ -279,12 +279,22 @@ export function module_illegal_default_export(node) {
}
/**
- * Cannot use `$props()` more than once
+ * Cannot use `%rune%()` more than once
+ * @param {null | number | NodeLike} node
+ * @param {string} rune
+ * @returns {never}
+ */
+export function props_duplicate(node, rune) {
+ e(node, 'props_duplicate', `Cannot use \`${rune}()\` more than once\nhttps://svelte.dev/e/props_duplicate`);
+}
+
+/**
+ * `$props.id()` can only be used at the top level of components as a variable declaration initializer
* @param {null | number | NodeLike} node
* @returns {never}
*/
-export function props_duplicate(node) {
- e(node, 'props_duplicate', `Cannot use \`$props()\` more than once\nhttps://svelte.dev/e/props_duplicate`);
+export function props_id_invalid_placement(node) {
+ e(node, 'props_id_invalid_placement', `\`$props.id()\` can only be used at the top level of components as a variable declaration initializer\nhttps://svelte.dev/e/props_id_invalid_placement`);
}
/**
diff --git a/packages/svelte/src/compiler/phases/1-parse/state/tag.js b/packages/svelte/src/compiler/phases/1-parse/state/tag.js
index 95d7d00677..0eb98c27e8 100644
--- a/packages/svelte/src/compiler/phases/1-parse/state/tag.js
+++ b/packages/svelte/src/compiler/phases/1-parse/state/tag.js
@@ -706,7 +706,7 @@ function special(parser) {
expression: /** @type {AST.RenderTag['expression']} */ (expression),
metadata: {
dynamic: false,
- args_with_call_expression: new Set(),
+ arguments: [],
path: [],
snippets: new Set()
}
diff --git a/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js b/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js
index ca7476ef7f..fc8108e46e 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js
+++ b/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js
@@ -339,13 +339,18 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element)
let sibling_elements; // do them lazy because it's rarely used and expensive to calculate
// If this is a :has inside a global selector, we gotta include the element itself, too,
- // because the global selector might be for an element that's outside the component (e.g. :root).
+ // because the global selector might be for an element that's outside the component,
+ // e.g. :root:has(.scoped), :global(.foo):has(.scoped), or :root { &:has(.scoped) {} }
const rules = get_parent_rules(rule);
const include_self =
rules.some((r) => r.prelude.children.some((c) => c.children.some((s) => is_global(s, r)))) ||
rules[rules.length - 1].prelude.children.some((c) =>
c.children.some((r) =>
- r.selectors.some((s) => s.type === 'PseudoClassSelector' && s.name === 'root')
+ r.selectors.some(
+ (s) =>
+ s.type === 'PseudoClassSelector' &&
+ (s.name === 'root' || (s.name === 'global' && s.args))
+ )
)
);
if (include_self) {
@@ -638,19 +643,30 @@ function get_following_sibling_elements(element, include_self) {
/** @type {Array} */
const siblings = [];
- // ...then walk them, starting from the node after the one
- // containing the element in question
+ // ...then walk them, starting from the node containing the element in question
+ // skipping nodes that appears before the element
const seen = new Set();
+ let skip = true;
/** @param {Compiler.AST.SvelteNode} node */
function get_siblings(node) {
walk(node, null, {
RegularElement(node) {
- siblings.push(node);
+ if (node === element) {
+ skip = false;
+ if (include_self) siblings.push(node);
+ } else if (!skip) {
+ siblings.push(node);
+ }
},
SvelteElement(node) {
- siblings.push(node);
+ if (node === element) {
+ skip = false;
+ if (include_self) siblings.push(node);
+ } else if (!skip) {
+ siblings.push(node);
+ }
},
RenderTag(node) {
for (const snippet of node.metadata.snippets) {
@@ -663,14 +679,10 @@ function get_following_sibling_elements(element, include_self) {
});
}
- for (const node of nodes.slice(nodes.indexOf(start) + 1)) {
+ for (const node of nodes.slice(nodes.indexOf(start))) {
get_siblings(node);
}
- if (include_self) {
- siblings.push(element);
- }
-
return siblings;
}
@@ -921,11 +933,9 @@ function get_possible_element_siblings(node, adjacent_only, seen = new Set()) {
/**
* @param {Compiler.AST.EachBlock | Compiler.AST.IfBlock | Compiler.AST.AwaitBlock | Compiler.AST.KeyBlock | Compiler.AST.SlotElement} node
* @param {boolean} adjacent_only
- * @returns {Map}
+ * @returns {Map}
*/
function get_possible_last_child(node, adjacent_only) {
- /** @typedef {Map} NodeMap */
-
/** @type {Array} */
let fragments = [];
@@ -948,7 +958,7 @@ function get_possible_last_child(node, adjacent_only) {
break;
}
- /** @type {NodeMap} */
+ /** @type {Map} NodeMap */
const result = new Map();
let exhaustive = node.type !== 'SlotElement';
@@ -989,9 +999,10 @@ function has_definite_elements(result) {
}
/**
- * @template T
- * @param {Map} from
- * @param {Map} to
+ * @template T2
+ * @template {T2} T1
+ * @param {Map} from
+ * @param {Map} to
* @returns {void}
*/
function add_to_map(from, to) {
@@ -1016,7 +1027,7 @@ function higher_existence(exist1, exist2) {
* @param {boolean} adjacent_only
*/
function loop_child(children, adjacent_only) {
- /** @type {Map} */
+ /** @type {Map} */
const result = new Map();
let i = children.length;
@@ -1029,6 +1040,8 @@ function loop_child(children, adjacent_only) {
if (adjacent_only) {
break;
}
+ } else if (child.type === 'SvelteElement') {
+ result.set(child, NODE_PROBABLY_EXISTS);
} else if (is_block(child)) {
const child_result = get_possible_last_child(child, adjacent_only);
add_to_map(child_result, result);
diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js
index 76c1e94277..846abcf7df 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/index.js
+++ b/packages/svelte/src/compiler/phases/2-analyze/index.js
@@ -243,7 +243,15 @@ export function analyze_module(ast, options) {
}
}
- const analysis = { runes: true, tracing: false };
+ /** @type {Analysis} */
+ const analysis = {
+ module: { ast, scope, scopes },
+ name: options.filename,
+ accessors: false,
+ runes: true,
+ immutable: true,
+ tracing: false
+ };
walk(
/** @type {Node} */ (ast),
@@ -256,14 +264,7 @@ export function analyze_module(ast, options) {
visitors
);
- return {
- module: { ast, scope, scopes },
- name: options.filename,
- accessors: false,
- runes: true,
- immutable: true,
- tracing: analysis.tracing
- };
+ return analysis;
}
/**
@@ -415,6 +416,7 @@ export function analyze_component(root, source, options) {
immutable: runes || options.immutable,
exports: [],
uses_props: false,
+ props_id: null,
uses_rest_props: false,
uses_slots: false,
uses_component_bindings: false,
@@ -604,8 +606,7 @@ export function analyze_component(root, source, options) {
has_props_rune: false,
component_slots: new Set(),
expression: null,
- render_tag: null,
- private_derived_state: [],
+ derived_state: [],
function_depth: scope.function_depth,
instance_scope: instance.scope,
reactive_statement: null,
@@ -676,8 +677,7 @@ export function analyze_component(root, source, options) {
reactive_statements: analysis.reactive_statements,
component_slots: new Set(),
expression: null,
- render_tag: null,
- private_derived_state: [],
+ derived_state: [],
function_depth: scope.function_depth
};
diff --git a/packages/svelte/src/compiler/phases/2-analyze/types.d.ts b/packages/svelte/src/compiler/phases/2-analyze/types.d.ts
index b4ca4dc262..14b14f9c84 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/types.d.ts
+++ b/packages/svelte/src/compiler/phases/2-analyze/types.d.ts
@@ -19,9 +19,7 @@ export interface AnalysisState {
component_slots: Set;
/** Information about the current expression/directive/block value */
expression: ExpressionMetadata | null;
- /** The current {@render ...} tag, if any */
- render_tag: null | AST.RenderTag;
- private_derived_state: string[];
+ derived_state: string[];
function_depth: number;
// legacy stuff
diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/Attribute.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/Attribute.js
index 41144fc74c..42e4498969 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/visitors/Attribute.js
+++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/Attribute.js
@@ -23,11 +23,6 @@ export function Attribute(node, context) {
if (node.name === 'value' && parent.name === 'option') {
mark_subtree_dynamic(context.path);
}
-
- // special case
- if (node.name === 'loading' && parent.name === 'img') {
- mark_subtree_dynamic(context.path);
- }
}
if (is_event_attribute(node)) {
diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js
index e9adebd81a..b6aebc9002 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js
+++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js
@@ -55,7 +55,7 @@ export function CallExpression(node, context) {
case '$props':
if (context.state.has_props_rune) {
- e.props_duplicate(node);
+ e.props_duplicate(node, rune);
}
context.state.has_props_rune = true;
@@ -74,12 +74,39 @@ export function CallExpression(node, context) {
break;
+ case '$props.id': {
+ const grand_parent = get_parent(context.path, -2);
+
+ if (context.state.analysis.props_id) {
+ e.props_duplicate(node, rune);
+ }
+
+ if (
+ parent.type !== 'VariableDeclarator' ||
+ parent.id.type !== 'Identifier' ||
+ context.state.ast_type !== 'instance' ||
+ context.state.scope !== context.state.analysis.instance.scope ||
+ grand_parent.type !== 'VariableDeclaration'
+ ) {
+ e.props_id_invalid_placement(node);
+ }
+
+ if (node.arguments.length > 0) {
+ e.rune_invalid_arguments(node, rune);
+ }
+
+ context.state.analysis.props_id = parent.id;
+
+ break;
+ }
+
case '$state':
case '$state.raw':
case '$derived':
case '$derived.by':
if (
- parent.type !== 'VariableDeclarator' &&
+ (parent.type !== 'VariableDeclarator' ||
+ get_parent(context.path, -3).type === 'ConstTag') &&
!(parent.type === 'PropertyDefinition' && !parent.static && !parent.computed)
) {
e.state_invalid_placement(node, rune);
@@ -191,18 +218,6 @@ export function CallExpression(node, context) {
break;
}
- if (context.state.render_tag) {
- // Find out which of the render tag arguments contains this call expression
- const arg_idx = unwrap_optional(context.state.render_tag.expression).arguments.findIndex(
- (arg) => arg === node || context.path.includes(arg)
- );
-
- // -1 if this is the call expression of the render tag itself
- if (arg_idx !== -1) {
- context.state.render_tag.metadata.args_with_call_expression.add(arg_idx);
- }
- }
-
if (node.callee.type === 'Identifier') {
const binding = context.state.scope.get(node.callee.name);
diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/ClassBody.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/ClassBody.js
index d445af0ebf..ed397258f8 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/visitors/ClassBody.js
+++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/ClassBody.js
@@ -8,20 +8,20 @@ import { get_rune } from '../../scope.js';
*/
export function ClassBody(node, context) {
/** @type {string[]} */
- const private_derived_state = [];
+ const derived_state = [];
for (const definition of node.body) {
if (
definition.type === 'PropertyDefinition' &&
- definition.key.type === 'PrivateIdentifier' &&
+ (definition.key.type === 'PrivateIdentifier' || definition.key.type === 'Identifier') &&
definition.value?.type === 'CallExpression'
) {
const rune = get_rune(definition.value, context.state.scope);
if (rune === '$derived' || rune === '$derived.by') {
- private_derived_state.push(definition.key.name);
+ derived_state.push(definition.key.name);
}
}
}
- context.next({ ...context.state, private_derived_state });
+ context.next({ ...context.state, derived_state });
}
diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/RenderTag.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/RenderTag.js
index 045224276a..a8c9d408bd 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/visitors/RenderTag.js
+++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/RenderTag.js
@@ -5,6 +5,7 @@ import * as e from '../../../errors.js';
import { validate_opening_tag } from './shared/utils.js';
import { mark_subtree_dynamic } from './shared/fragment.js';
import { is_resolved_snippet } from './shared/snippets.js';
+import { create_expression_metadata } from '../../nodes.js';
/**
* @param {AST.RenderTag} node
@@ -15,7 +16,8 @@ export function RenderTag(node, context) {
node.metadata.path = [...context.path];
- const callee = unwrap_optional(node.expression).callee;
+ const expression = unwrap_optional(node.expression);
+ const callee = expression.callee;
const binding = callee.type === 'Identifier' ? context.state.scope.get(callee.name) : null;
@@ -52,5 +54,15 @@ export function RenderTag(node, context) {
mark_subtree_dynamic(context.path);
- context.next({ ...context.state, render_tag: node });
+ context.visit(callee);
+
+ for (const arg of expression.arguments) {
+ const metadata = create_expression_metadata();
+ node.metadata.arguments.push(metadata);
+
+ context.visit(arg, {
+ ...context.state,
+ expression: metadata
+ });
+ }
}
diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/a11y.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/a11y.js
index a5ca8463a4..24a8e5122d 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/a11y.js
+++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/a11y.js
@@ -756,7 +756,8 @@ export function check_element(node, context) {
name === 'aria-activedescendant' &&
!is_dynamic_element &&
!is_interactive_element(node.name, attribute_map) &&
- !attribute_map.has('tabindex')
+ !attribute_map.has('tabindex') &&
+ !has_spread
) {
w.a11y_aria_activedescendant_has_tabindex(attribute);
}
@@ -810,9 +811,9 @@ export function check_element(node, context) {
const role = roles_map.get(current_role);
if (role) {
const required_role_props = Object.keys(role.requiredProps);
- const has_missing_props = required_role_props.some(
- (prop) => !attributes.find((a) => a.name === prop)
- );
+ const has_missing_props =
+ !has_spread &&
+ required_role_props.some((prop) => !attributes.find((a) => a.name === prop));
if (has_missing_props) {
w.a11y_role_has_required_aria_props(
attribute,
@@ -828,6 +829,7 @@ export function check_element(node, context) {
// interactive-supports-focus
if (
+ !has_spread &&
!has_disabled_attribute(attribute_map) &&
!is_hidden_from_screen_reader(node.name, attribute_map) &&
!is_presentation_role(current_role) &&
@@ -845,6 +847,7 @@ export function check_element(node, context) {
// no-interactive-element-to-noninteractive-role
if (
+ !has_spread &&
is_interactive_element(node.name, attribute_map) &&
(is_non_interactive_roles(current_role) || is_presentation_role(current_role))
) {
@@ -853,6 +856,7 @@ export function check_element(node, context) {
// no-noninteractive-element-to-interactive-role
if (
+ !has_spread &&
is_non_interactive_element(node.name, attribute_map) &&
is_interactive_roles(current_role) &&
!a11y_non_interactive_element_to_interactive_role_exceptions[node.name]?.includes(
@@ -947,6 +951,7 @@ export function check_element(node, context) {
// no-noninteractive-element-interactions
if (
+ !has_spread &&
!has_contenteditable_attr &&
!is_hidden_from_screen_reader(node.name, attribute_map) &&
!is_presentation_role(role_static_value) &&
@@ -964,6 +969,7 @@ export function check_element(node, context) {
// no-static-element-interactions
if (
+ !has_spread &&
(!role || role_static_value !== null) &&
!is_hidden_from_screen_reader(node.name, attribute_map) &&
!is_presentation_role(role_static_value) &&
@@ -981,11 +987,11 @@ export function check_element(node, context) {
}
}
- if (handlers.has('mouseover') && !handlers.has('focus')) {
+ if (!has_spread && handlers.has('mouseover') && !handlers.has('focus')) {
w.a11y_mouse_events_have_key_events(node, 'mouseover', 'focus');
}
- if (handlers.has('mouseout') && !handlers.has('blur')) {
+ if (!has_spread && handlers.has('mouseout') && !handlers.has('blur')) {
w.a11y_mouse_events_have_key_events(node, 'mouseout', 'blur');
}
@@ -995,7 +1001,7 @@ export function check_element(node, context) {
if (node.name === 'a' || node.name === 'button') {
const is_hidden = get_static_value(attribute_map.get('aria-hidden')) === 'true';
- if (!is_hidden && !is_labelled && !has_content(node)) {
+ if (!has_spread && !is_hidden && !is_labelled && !has_content(node)) {
w.a11y_consider_explicit_label(node);
}
}
@@ -1054,7 +1060,7 @@ export function check_element(node, context) {
if (node.name === 'img') {
const alt_attribute = get_static_text_value(attribute_map.get('alt'));
const aria_hidden = get_static_value(attribute_map.get('aria-hidden'));
- if (alt_attribute && !aria_hidden) {
+ if (alt_attribute && !aria_hidden && !has_spread) {
if (/\b(image|picture|photo)\b/i.test(alt_attribute)) {
w.a11y_img_redundant_alt(node);
}
@@ -1087,7 +1093,7 @@ export function check_element(node, context) {
);
return has;
};
- if (!attribute_map.has('for') && !has_input_child(node)) {
+ if (!has_spread && !attribute_map.has('for') && !has_input_child(node)) {
w.a11y_label_has_associated_control(node);
}
}
@@ -1095,7 +1101,7 @@ export function check_element(node, context) {
if (node.name === 'video') {
const aria_hidden_attribute = attribute_map.get('aria-hidden');
const aria_hidden_exist = aria_hidden_attribute && get_static_value(aria_hidden_attribute);
- if (attribute_map.has('muted') || aria_hidden_exist === 'true') {
+ if (attribute_map.has('muted') || aria_hidden_exist === 'true' || has_spread) {
return;
}
let has_caption = false;
@@ -1141,6 +1147,7 @@ export function check_element(node, context) {
// Check content
if (
+ !has_spread &&
!is_labelled &&
!has_contenteditable_binding &&
a11y_required_content.includes(node.name) &&
diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/attribute.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/attribute.js
index 198e464ac7..19bd7b6e54 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/attribute.js
+++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/attribute.js
@@ -80,40 +80,42 @@ export function validate_slot_attribute(context, attribute, is_component = false
}
if (owner) {
- if (!is_text_attribute(attribute)) {
- e.slot_attribute_invalid(attribute);
- }
-
if (
owner.type === 'Component' ||
owner.type === 'SvelteComponent' ||
owner.type === 'SvelteSelf'
) {
if (owner !== parent) {
- e.slot_attribute_invalid_placement(attribute);
- }
+ if (!is_component) {
+ e.slot_attribute_invalid_placement(attribute);
+ }
+ } else {
+ if (!is_text_attribute(attribute)) {
+ e.slot_attribute_invalid(attribute);
+ }
- const name = attribute.value[0].data;
+ const name = attribute.value[0].data;
- if (context.state.component_slots.has(name)) {
- e.slot_attribute_duplicate(attribute, name, owner.name);
- }
-
- context.state.component_slots.add(name);
+ if (context.state.component_slots.has(name)) {
+ e.slot_attribute_duplicate(attribute, name, owner.name);
+ }
- if (name === 'default') {
- for (const node of owner.fragment.nodes) {
- if (node.type === 'Text' && regex_only_whitespaces.test(node.data)) {
- continue;
- }
+ context.state.component_slots.add(name);
- if (node.type === 'RegularElement' || node.type === 'SvelteFragment') {
- if (node.attributes.some((a) => a.type === 'Attribute' && a.name === 'slot')) {
+ if (name === 'default') {
+ for (const node of owner.fragment.nodes) {
+ if (node.type === 'Text' && regex_only_whitespaces.test(node.data)) {
continue;
}
- }
- e.slot_default_duplicate(node);
+ if (node.type === 'RegularElement' || node.type === 'SvelteFragment') {
+ if (node.attributes.some((a) => a.type === 'Attribute' && a.name === 'slot')) {
+ continue;
+ }
+ }
+
+ e.slot_default_duplicate(node);
+ }
}
}
}
diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/utils.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/utils.js
index e265637c40..2d90c85364 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/utils.js
+++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/utils.js
@@ -25,6 +25,10 @@ export function validate_assignment(node, argument, state) {
e.constant_assignment(node, 'derived state');
}
+ if (binding?.node === state.analysis.props_id) {
+ e.constant_assignment(node, '$props.id()');
+ }
+
if (binding?.kind === 'each') {
e.each_item_invalid_assignment(node);
}
@@ -35,20 +39,17 @@ export function validate_assignment(node, argument, state) {
}
}
- let object = /** @type {Expression | Super} */ (argument);
-
- /** @type {Expression | PrivateIdentifier | null} */
- let property = null;
-
- while (object.type === 'MemberExpression') {
- property = object.property;
- object = object.object;
- }
-
- if (object.type === 'ThisExpression' && property?.type === 'PrivateIdentifier') {
- if (state.private_derived_state.includes(property.name)) {
- e.constant_assignment(node, 'derived state');
- }
+ if (
+ argument.type === 'MemberExpression' &&
+ argument.object.type === 'ThisExpression' &&
+ (((argument.property.type === 'PrivateIdentifier' || argument.property.type === 'Identifier') &&
+ state.derived_state.includes(argument.property.name)) ||
+ (argument.property.type === 'Literal' &&
+ argument.property.value &&
+ typeof argument.property.value === 'string' &&
+ state.derived_state.includes(argument.property.value)))
+ ) {
+ e.constant_assignment(node, 'derived state');
}
}
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js
index e7ac6a9653..2b8a3f2945 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js
@@ -565,6 +565,11 @@ export function client_component(analysis, options) {
component_block.body.unshift(b.stmt(b.call('$.check_target', b.id('new.target'))));
}
+ if (analysis.props_id) {
+ // need to be placed on first line of the component for hydration
+ component_block.body.unshift(b.const(analysis.props_id, b.call('$.props_id')));
+ }
+
if (state.events.size > 0) {
body.push(
b.stmt(b.call('$.delegate', b.array(Array.from(state.events).map((name) => b.literal(name)))))
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/utils.js b/packages/svelte/src/compiler/phases/3-transform/client/utils.js
index 664b909a09..28e3fabb19 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/utils.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/utils.js
@@ -261,41 +261,6 @@ export function should_proxy(node, scope) {
return true;
}
-/**
- * @param {Pattern} node
- * @param {import('zimmerframe').Context} context
- * @returns {{ id: Pattern, declarations: null | Statement[] }}
- */
-export function create_derived_block_argument(node, context) {
- if (node.type === 'Identifier') {
- context.state.transform[node.name] = { read: get_value };
- return { id: node, declarations: null };
- }
-
- const pattern = /** @type {Pattern} */ (context.visit(node));
- const identifiers = extract_identifiers(node);
-
- const id = b.id('$$source');
- const value = b.id('$$value');
-
- const block = b.block([
- b.var(pattern, b.call('$.get', id)),
- b.return(b.object(identifiers.map((identifier) => b.prop('init', identifier, identifier))))
- ]);
-
- const declarations = [b.var(value, create_derived(context.state, b.thunk(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))))
- );
- }
-
- return { id, declarations };
-}
-
/**
* Svelte legacy mode should use safe equals in most places, runes mode shouldn't
* @param {ComponentClientTransformState} state
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitBlock.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitBlock.js
index 146f75d405..e0aef2d316 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitBlock.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitBlock.js
@@ -1,8 +1,10 @@
/** @import { BlockStatement, Expression, Pattern, Statement } from 'estree' */
/** @import { AST } from '#compiler' */
-/** @import { ComponentContext } from '../types' */
+/** @import { ComponentClientTransformState, ComponentContext } from '../types' */
+import { extract_identifiers } from '../../../../utils/ast.js';
import * as b from '../../../../utils/builders.js';
-import { create_derived_block_argument } from '../utils.js';
+import { create_derived } from '../utils.js';
+import { get_value } from './shared/declarations.js';
/**
* @param {AST.AwaitBlock} node
@@ -65,3 +67,38 @@ export function AwaitBlock(node, context) {
)
);
}
+
+/**
+ * @param {Pattern} node
+ * @param {import('zimmerframe').Context} context
+ * @returns {{ id: Pattern, declarations: null | Statement[] }}
+ */
+function create_derived_block_argument(node, context) {
+ if (node.type === 'Identifier') {
+ context.state.transform[node.name] = { read: get_value };
+ return { id: node, declarations: null };
+ }
+
+ const pattern = /** @type {Pattern} */ (context.visit(node));
+ const identifiers = extract_identifiers(node);
+
+ const id = b.id('$$source');
+ const value = b.id('$$value');
+
+ const block = b.block([
+ b.var(pattern, b.call('$.get', id)),
+ b.return(b.object(identifiers.map((identifier) => b.prop('init', identifier, identifier))))
+ ]);
+
+ const declarations = [b.var(value, create_derived(context.state, b.thunk(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))))
+ );
+ }
+
+ return { id, declarations };
+}
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ClassBody.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ClassBody.js
index e9a1fec0c6..975ada4111 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ClassBody.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ClassBody.js
@@ -198,22 +198,21 @@ export function ClassBody(node, context) {
'method',
b.id('$.ADD_OWNER'),
[b.id('owner')],
- Array.from(public_state)
- // Only run ownership addition on $state fields.
- // Theoretically someone could create a `$state` while creating `$state.raw` or inside a `$derived.by`,
- // but that feels so much of an edge case that it doesn't warrant a perf hit for the common case.
- .filter(([_, { kind }]) => kind === 'state')
- .map(([name]) =>
- b.stmt(
- b.call(
- '$.add_owner',
- b.call('$.get', b.member(b.this, b.private_id(name))),
- b.id('owner'),
- b.literal(false),
- is_ignored(node, 'ownership_invalid_binding') && b.true
- )
+ [
+ b.stmt(
+ b.call(
+ '$.add_owner_to_class',
+ b.this,
+ b.id('owner'),
+ b.array(
+ Array.from(public_state).map(([name]) =>
+ b.thunk(b.call('$.get', b.member(b.this, b.private_id(name))))
+ )
+ ),
+ is_ignored(node, 'ownership_invalid_binding') && b.true
)
- ),
+ )
+ ],
true
)
);
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js
index 9f70981205..24a696e7d2 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js
@@ -35,10 +35,6 @@ export function EachBlock(node, context) {
context.state.template.push('');
}
- if (each_node_meta.array_name !== null) {
- context.state.init.push(b.const(each_node_meta.array_name, b.thunk(collection)));
- }
-
let flags = 0;
if (node.metadata.keyed && node.index) {
@@ -120,8 +116,21 @@ export function EachBlock(node, context) {
return [array, ...transitive_dependencies];
});
- if (each_node_meta.array_name) {
- indirect_dependencies.push(b.call(each_node_meta.array_name));
+ /** @type {Identifier | null} */
+ let collection_id = null;
+
+ // Check if inner scope shadows something from outer scope.
+ // This is necessary because we need access to the array expression of the each block
+ // in the inner scope if bindings are used, in order to invalidate the array.
+ for (const [name] of context.state.scope.declarations) {
+ if (context.state.scope.parent?.get(name) != null) {
+ collection_id = context.state.scope.root.unique('$$array');
+ break;
+ }
+ }
+
+ if (collection_id) {
+ indirect_dependencies.push(b.call(collection_id));
} else {
indirect_dependencies.push(collection);
@@ -195,7 +204,7 @@ export function EachBlock(node, context) {
// TODO 6.0 this only applies in legacy mode, reassignments are
// forbidden in runes mode
return b.member(
- each_node_meta.array_name ? b.call(each_node_meta.array_name) : collection,
+ collection_id ? b.call(collection_id) : collection,
(flags & EACH_INDEX_REACTIVE) !== 0 ? get_value(index) : index,
true
);
@@ -207,7 +216,7 @@ export function EachBlock(node, context) {
uses_index = true;
const left = b.member(
- each_node_meta.array_name ? b.call(each_node_meta.array_name) : collection,
+ collection_id ? b.call(collection_id) : collection,
(flags & EACH_INDEX_REACTIVE) !== 0 ? get_value(index) : index,
true
);
@@ -283,16 +292,17 @@ export function EachBlock(node, context) {
);
}
+ const render_args = [b.id('$$anchor'), item];
+ if (uses_index || collection_id) render_args.push(index);
+ if (collection_id) render_args.push(collection_id);
+
/** @type {Expression[]} */
const args = [
context.state.node,
b.literal(flags),
- each_node_meta.array_name ? each_node_meta.array_name : b.thunk(collection),
+ b.thunk(collection),
key_function,
- b.arrow(
- uses_index ? [b.id('$$anchor'), item, index] : [b.id('$$anchor'), item],
- b.block(declarations.concat(block.body))
- )
+ b.arrow(render_args, b.block(declarations.concat(block.body)))
];
if (node.fallback) {
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js
index 21a78de032..98036aa9b6 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js
@@ -227,7 +227,7 @@ export function RegularElement(node, context) {
node_id,
attributes_id,
(node.metadata.svg || node.metadata.mathml || is_custom_element_node(node)) && b.true,
- node.name.includes('-') && b.true,
+ is_custom_element_node(node) && b.true,
context.state
);
@@ -300,11 +300,6 @@ export function RegularElement(node, context) {
build_class_directives(class_directives, node_id, context, is_attributes_reactive);
build_style_directives(style_directives, node_id, context, is_attributes_reactive);
- // Apply the src and loading attributes for elements after the element is appended to the document
- if (node.name === 'img' && (has_spread || lookup.has('loading'))) {
- context.state.after_update.push(b.stmt(b.call('$.handle_lazy_img', node_id)));
- }
-
if (
is_load_error_element(node.name) &&
(has_spread || has_use || lookup.has('onload') || lookup.has('onerror'))
@@ -537,8 +532,8 @@ function build_element_attribute_update_assignment(
const is_svg = context.state.metadata.namespace === 'svg' || element.name === 'svg';
const is_mathml = context.state.metadata.namespace === 'mathml';
- let { value, has_state } = build_attribute_value(attribute.value, context, (value) =>
- get_expression_id(state, value)
+ let { value, has_state } = build_attribute_value(attribute.value, context, (value, metadata) =>
+ metadata.has_call ? get_expression_id(state, value) : value
);
if (name === 'autofocus') {
@@ -665,8 +660,8 @@ function build_custom_element_attribute_update_assignment(node_id, attribute, co
*/
function build_element_special_value_attribute(element, node_id, attribute, context) {
const state = context.state;
- const { value, has_state } = build_attribute_value(attribute.value, context, (value) =>
- get_expression_id(state, value)
+ const { value, has_state } = build_attribute_value(attribute.value, context, (value, metadata) =>
+ metadata.has_call ? get_expression_id(state, value) : value
);
const inner_assignment = b.assignment(
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RenderTag.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RenderTag.js
index 7da987f6cc..33ae6d4d2b 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RenderTag.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RenderTag.js
@@ -10,20 +10,24 @@ import * as b from '../../../../utils/builders.js';
*/
export function RenderTag(node, context) {
context.state.template.push('');
- const callee = unwrap_optional(node.expression).callee;
- const raw_args = unwrap_optional(node.expression).arguments;
+
+ const expression = unwrap_optional(node.expression);
+
+ const callee = expression.callee;
+ const raw_args = expression.arguments;
/** @type {Expression[]} */
let args = [];
for (let i = 0; i < raw_args.length; i++) {
- const raw = raw_args[i];
- const arg = /** @type {Expression} */ (context.visit(raw));
- if (node.metadata.args_with_call_expression.has(i)) {
+ let thunk = b.thunk(/** @type {Expression} */ (context.visit(raw_args[i])));
+ const { has_call } = node.metadata.arguments[i];
+
+ if (has_call) {
const id = b.id(context.state.scope.generate('render_arg'));
- context.state.init.push(b.var(id, b.call('$.derived_safe_equal', b.thunk(arg))));
+ context.state.init.push(b.var(id, b.call('$.derived_safe_equal', thunk)));
args.push(b.thunk(b.call('$.get', id)));
} else {
- args.push(b.thunk(arg));
+ args.push(thunk);
}
}
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SlotElement.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SlotElement.js
index f1b08acbc6..fdd705e32e 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SlotElement.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SlotElement.js
@@ -30,8 +30,10 @@ export function SlotElement(node, context) {
if (attribute.type === 'SpreadAttribute') {
spreads.push(b.thunk(/** @type {Expression} */ (context.visit(attribute))));
} else if (attribute.type === 'Attribute') {
- const { value, has_state } = build_attribute_value(attribute.value, context, (value) =>
- memoize_expression(context.state, value)
+ const { value, has_state } = build_attribute_value(
+ attribute.value,
+ context,
+ (value, metadata) => (metadata.has_call ? memoize_expression(context.state, value) : value)
);
if (attribute.name === 'name') {
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js
index 6897f554e2..d636e42cde 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js
@@ -42,6 +42,11 @@ export function VariableDeclaration(node, context) {
continue;
}
+ if (rune === '$props.id') {
+ // skip
+ continue;
+ }
+
if (rune === '$props') {
/** @type {string[]} */
const seen = ['$$slots', '$$events', '$$legacy'];
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js
index 9ac0bac120..2bae4486dc 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js
@@ -4,7 +4,6 @@
import { dev, is_ignored } from '../../../../../state.js';
import { get_attribute_chunks, object } from '../../../../../utils/ast.js';
import * as b from '../../../../../utils/builders.js';
-import { create_derived } from '../../utils.js';
import { build_bind_this, memoize_expression, validate_binding } from '../shared/utils.js';
import { build_attribute_value } from '../shared/element.js';
import { build_event_handler } from './events.js';
@@ -134,9 +133,9 @@ export function build_component(node, component_name, context, anchor = context.
custom_css_props.push(
b.init(
attribute.name,
- build_attribute_value(attribute.value, context, (value) =>
+ build_attribute_value(attribute.value, context, (value, metadata) =>
// TODO put the derived in the local block
- memoize_expression(context.state, value)
+ metadata.has_call ? memoize_expression(context.state, value) : value
).value
)
);
@@ -151,31 +150,29 @@ export function build_component(node, component_name, context, anchor = context.
has_children_prop = true;
}
- const { value, has_state } = build_attribute_value(attribute.value, context, (value) =>
- memoize_expression(context.state, value)
- );
-
- if (has_state) {
- let arg = value;
-
- // When we have a non-simple computation, anything other than an Identifier or Member expression,
- // then there's a good chance it needs to be memoized to avoid over-firing when read within the
- // child component.
- const should_wrap_in_derived = get_attribute_chunks(attribute.value).some((n) => {
- return (
- n.type === 'ExpressionTag' &&
- n.expression.type !== 'Identifier' &&
- n.expression.type !== 'MemberExpression'
- );
- });
+ const { value, has_state } = build_attribute_value(
+ attribute.value,
+ context,
+ (value, metadata) => {
+ if (!metadata.has_state) return value;
+
+ // When we have a non-simple computation, anything other than an Identifier or Member expression,
+ // then there's a good chance it needs to be memoized to avoid over-firing when read within the
+ // child component (e.g. `active={i === index}`)
+ const should_wrap_in_derived = get_attribute_chunks(attribute.value).some((n) => {
+ return (
+ n.type === 'ExpressionTag' &&
+ n.expression.type !== 'Identifier' &&
+ n.expression.type !== 'MemberExpression'
+ );
+ });
- if (should_wrap_in_derived) {
- const id = b.id(context.state.scope.generate(attribute.name));
- context.state.init.push(b.var(id, create_derived(context.state, b.thunk(value))));
- arg = b.call('$.get', id);
+ return should_wrap_in_derived ? memoize_expression(context.state, value) : value;
}
+ );
- push_prop(b.get(attribute.name, [b.return(arg)]));
+ if (has_state) {
+ push_prop(b.get(attribute.name, [b.return(value)]));
} else {
push_prop(b.init(attribute.name, value));
}
@@ -183,37 +180,18 @@ export function build_component(node, component_name, context, anchor = context.
const expression = /** @type {Expression} */ (context.visit(attribute.expression));
if (dev && attribute.name !== 'this') {
- let should_add_owner = true;
-
- if (attribute.expression.type !== 'SequenceExpression') {
- const left = object(attribute.expression);
-
- if (left?.type === 'Identifier') {
- const binding = context.state.scope.get(left.name);
-
- // Only run ownership addition on $state fields.
- // Theoretically someone could create a `$state` while creating `$state.raw` or inside a `$derived.by`,
- // but that feels so much of an edge case that it doesn't warrant a perf hit for the common case.
- if (binding?.kind === 'derived' || binding?.kind === 'raw_state') {
- should_add_owner = false;
- }
- }
- }
-
- if (should_add_owner) {
- binding_initializers.push(
- b.stmt(
- b.call(
- b.id('$.add_owner_effect'),
- expression.type === 'SequenceExpression'
- ? expression.expressions[0]
- : b.thunk(expression),
- b.id(component_name),
- is_ignored(node, 'ownership_invalid_binding') && b.true
- )
+ binding_initializers.push(
+ b.stmt(
+ b.call(
+ b.id('$.add_owner_effect'),
+ expression.type === 'SequenceExpression'
+ ? expression.expressions[0]
+ : b.thunk(expression),
+ b.id(component_name),
+ is_ignored(node, 'ownership_invalid_binding') && b.true
)
- );
- }
+ )
+ );
}
if (expression.type === 'SequenceExpression') {
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js
index 8fb6b8bdde..abffad0ff7 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js
@@ -1,11 +1,11 @@
/** @import { Expression, Identifier, ObjectExpression } from 'estree' */
-/** @import { AST } from '#compiler' */
+/** @import { AST, ExpressionMetadata } from '#compiler' */
/** @import { ComponentClientTransformState, ComponentContext } from '../../types' */
import { normalize_attribute } from '../../../../../../utils.js';
import { is_ignored } from '../../../../../state.js';
import { is_event_attribute } from '../../../../../utils/ast.js';
import * as b from '../../../../../utils/builders.js';
-import { build_getter, create_derived } from '../../utils.js';
+import { build_getter } from '../../utils.js';
import { build_template_chunk, get_expression_id } from './utils.js';
/**
@@ -35,8 +35,10 @@ export function build_set_attributes(
for (const attribute of attributes) {
if (attribute.type === 'Attribute') {
- const { value, has_state } = build_attribute_value(attribute.value, context, (value) =>
- get_expression_id(context.state, value)
+ const { value, has_state } = build_attribute_value(
+ attribute.value,
+ context,
+ (value, metadata) => (metadata.has_call ? get_expression_id(context.state, value) : value)
);
if (
@@ -59,10 +61,9 @@ export function build_set_attributes(
let value = /** @type {Expression} */ (context.visit(attribute));
if (attribute.metadata.expression.has_call) {
- const id = b.id(state.scope.generate('spread_with_call'));
- state.init.push(b.const(id, create_derived(state, b.thunk(value))));
- value = b.call('$.get', id);
+ value = get_expression_id(context.state, value);
}
+
values.push(b.spread(value));
}
}
@@ -111,8 +112,8 @@ export function build_style_directives(
let value =
directive.value === true
? build_getter({ name: directive.name, type: 'Identifier' }, context.state)
- : build_attribute_value(directive.value, context, (value) =>
- get_expression_id(context.state, value)
+ : build_attribute_value(directive.value, context, (value, metadata) =>
+ metadata.has_call ? get_expression_id(context.state, value) : value
).value;
const update = b.stmt(
@@ -169,7 +170,7 @@ export function build_class_directives(
/**
* @param {AST.Attribute['value']} value
* @param {ComponentContext} context
- * @param {(value: Expression) => Expression} memoize
+ * @param {(value: Expression, metadata: ExpressionMetadata) => Expression} memoize
* @returns {{ value: Expression, has_state: boolean }}
*/
export function build_attribute_value(value, context, memoize = (value) => value) {
@@ -187,7 +188,7 @@ export function build_attribute_value(value, context, memoize = (value) => value
let expression = /** @type {Expression} */ (context.visit(chunk.expression));
return {
- value: chunk.metadata.expression.has_call ? memoize(expression) : expression,
+ value: memoize(expression, chunk.metadata.expression),
has_state: chunk.metadata.expression.has_state
};
}
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/fragment.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/fragment.js
index 0b4ac87342..5bc3041ca4 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/fragment.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/fragment.js
@@ -4,6 +4,7 @@
import { cannot_be_set_statically } from '../../../../../../utils.js';
import { is_event_attribute, is_text_attribute } from '../../../../../utils/ast.js';
import * as b from '../../../../../utils/builders.js';
+import { is_custom_element_node } from '../../../../nodes.js';
import { build_template_chunk } from './utils.js';
/**
@@ -128,7 +129,7 @@ export function process_children(nodes, initial, is_element, { visit, state }) {
function is_static_element(node, state) {
if (node.type !== 'RegularElement') return false;
if (node.fragment.metadata.dynamic) return false;
- if (node.name.includes('-')) return false; // we're setting all attributes on custom elements through properties
+ if (is_custom_element_node(node)) return false; // we're setting all attributes on custom elements through properties
for (const attribute of node.attributes) {
if (attribute.type !== 'Attribute') {
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js
index c4f81274d9..9214a13c94 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js
@@ -1,5 +1,5 @@
/** @import { Expression, ExpressionStatement, Identifier, MemberExpression, SequenceExpression, Statement, Super } from 'estree' */
-/** @import { AST } from '#compiler' */
+/** @import { AST, ExpressionMetadata } from '#compiler' */
/** @import { ComponentClientTransformState } from '../../types' */
import { walk } from 'zimmerframe';
import { object } from '../../../../../utils/ast.js';
@@ -79,14 +79,14 @@ function compare_expressions(a, b) {
* @param {Array} values
* @param {(node: AST.SvelteNode, state: any) => any} visit
* @param {ComponentClientTransformState} state
- * @param {(value: Expression) => Expression} memoize
+ * @param {(value: Expression, metadata: ExpressionMetadata) => Expression} memoize
* @returns {{ value: Expression, has_state: boolean }}
*/
export function build_template_chunk(
values,
visit,
state,
- memoize = (value) => get_expression_id(state, value)
+ memoize = (value, metadata) => (metadata.has_call ? get_expression_id(state, value) : value)
) {
/** @type {Expression[]} */
const expressions = [];
@@ -106,35 +106,40 @@ export function build_template_chunk(
quasi.value.cooked += node.expression.value + '';
}
} else {
- let value = /** @type {Expression} */ (visit(node.expression, state));
+ let value = memoize(
+ /** @type {Expression} */ (visit(node.expression, state)),
+ node.metadata.expression
+ );
has_state ||= node.metadata.expression.has_state;
- if (node.metadata.expression.has_call) {
- value = memoize(value);
- }
-
if (values.length === 1) {
// If we have a single expression, then pass that in directly to possibly avoid doing
// extra work in the template_effect (instead we do the work in set_text).
return { value, has_state };
} else {
- let expression = value;
- // only add nullish coallescence if it hasn't been added already
- if (value.type === 'LogicalExpression' && value.operator === '??') {
- const { right } = value;
- // `undefined` isn't a Literal (due to pre-ES5 shenanigans), so the only nullish literal is `null`
- // however, you _can_ make a variable called `undefined` in a Svelte component, so we can't just treat it the same way
- if (right.type !== 'Literal') {
- expression = b.logical('??', value, b.literal(''));
- } else if (right.value === null) {
- // if they do something weird like `stuff ?? null`, replace `null` with empty string
- value.right = b.literal('');
+ // add `?? ''` where necessary (TODO optimise more cases)
+ if (
+ value.type === 'LogicalExpression' &&
+ value.right.type === 'Literal' &&
+ (value.operator === '??' || value.operator === '||')
+ ) {
+ // `foo ?? null` -=> `foo ?? ''`
+ // otherwise leave the expression untouched
+ if (value.right.value === null) {
+ value = { ...value, right: b.literal('') };
}
+ } else if (
+ state.analysis.props_id &&
+ value.type === 'Identifier' &&
+ value.name === state.analysis.props_id.name
+ ) {
+ // do nothing ($props.id() is never null/undefined)
} else {
- expression = b.logical('??', value, b.literal(''));
+ value = b.logical('??', value, b.literal(''));
}
- expressions.push(expression);
+
+ expressions.push(value);
}
quasi = b.quasi('', i + 1 === values.length);
diff --git a/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js b/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js
index 982b75e12f..df3d831d3c 100644
--- a/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js
+++ b/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js
@@ -244,6 +244,13 @@ export function server_component(analysis, options) {
.../** @type {Statement[]} */ (template.body)
]);
+ if (analysis.props_id) {
+ // need to be placed on first line of the component for hydration
+ component_block.body.unshift(
+ b.const(analysis.props_id, b.call('$.props_id', b.id('$$payload')))
+ );
+ }
+
let should_inject_context = dev || analysis.needs_context;
if (should_inject_context) {
diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/VariableDeclaration.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/VariableDeclaration.js
index 31de811ac7..c4c31d7eb3 100644
--- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/VariableDeclaration.js
+++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/VariableDeclaration.js
@@ -24,6 +24,11 @@ export function VariableDeclaration(node, context) {
continue;
}
+ if (rune === '$props.id') {
+ // skip
+ continue;
+ }
+
if (rune === '$props') {
let has_rest = false;
// remove $bindable() from props declaration
@@ -156,6 +161,10 @@ export function VariableDeclaration(node, context) {
}
}
+ if (declarations.length === 0) {
+ return b.empty;
+ }
+
return {
...node,
declarations
diff --git a/packages/svelte/src/compiler/phases/nodes.js b/packages/svelte/src/compiler/phases/nodes.js
index 5ca4ce3380..003c3a2c49 100644
--- a/packages/svelte/src/compiler/phases/nodes.js
+++ b/packages/svelte/src/compiler/phases/nodes.js
@@ -23,10 +23,14 @@ export function is_element_node(node) {
/**
* @param {AST.RegularElement | AST.SvelteElement} node
- * @returns {node is AST.RegularElement}
+ * @returns {boolean}
*/
export function is_custom_element_node(node) {
- return node.type === 'RegularElement' && node.name.includes('-');
+ return (
+ node.type === 'RegularElement' &&
+ (node.name.includes('-') ||
+ node.attributes.some((attr) => attr.type === 'Attribute' && attr.name === 'is'))
+ );
}
/**
diff --git a/packages/svelte/src/compiler/phases/scope.js b/packages/svelte/src/compiler/phases/scope.js
index 3536dd6a18..51e9eb088d 100644
--- a/packages/svelte/src/compiler/phases/scope.js
+++ b/packages/svelte/src/compiler/phases/scope.js
@@ -575,21 +575,10 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
}
if (node.fallback) visit(node.fallback, { scope });
- // Check if inner scope shadows something from outer scope.
- // This is necessary because we need access to the array expression of the each block
- // in the inner scope if bindings are used, in order to invalidate the array.
- let needs_array_deduplication = false;
- for (const [name] of scope.declarations) {
- if (state.scope.get(name) !== null) {
- needs_array_deduplication = true;
- }
- }
-
node.metadata = {
expression: create_expression_metadata(),
keyed: false,
contains_group_binding: false,
- array_name: needs_array_deduplication ? state.scope.root.unique('$$array') : null,
index: scope.root.unique('$$index'),
declarations: scope.declarations,
is_controlled: false
diff --git a/packages/svelte/src/compiler/phases/types.d.ts b/packages/svelte/src/compiler/phases/types.d.ts
index fe32dbba3e..abe2b115de 100644
--- a/packages/svelte/src/compiler/phases/types.d.ts
+++ b/packages/svelte/src/compiler/phases/types.d.ts
@@ -44,6 +44,8 @@ export interface ComponentAnalysis extends Analysis {
exports: Array<{ name: string; alias: string | null }>;
/** Whether the component uses `$$props` */
uses_props: boolean;
+ /** The component ID variable name, if any */
+ props_id: Identifier | null;
/** Whether the component uses `$$restProps` */
uses_rest_props: boolean;
/** Whether the component uses `$$slots` */
diff --git a/packages/svelte/src/compiler/types/template.d.ts b/packages/svelte/src/compiler/types/template.d.ts
index fb60966895..a544cd1dec 100644
--- a/packages/svelte/src/compiler/types/template.d.ts
+++ b/packages/svelte/src/compiler/types/template.d.ts
@@ -166,7 +166,7 @@ export namespace AST {
/** @internal */
metadata: {
dynamic: boolean;
- args_with_call_expression: Set;
+ arguments: ExpressionMetadata[];
path: SvelteNode[];
/** The set of locally-defined snippets that this render tag could correspond to,
* used for CSS pruning purposes */
@@ -414,8 +414,6 @@ export namespace AST {
expression: ExpressionMetadata;
keyed: boolean;
contains_group_binding: boolean;
- /** Set if something in the array expression is shadowed within the each block */
- array_name: Identifier | null;
index: Identifier;
declarations: Map;
/**
diff --git a/packages/svelte/src/index-client.js b/packages/svelte/src/index-client.js
index 587d766233..ca29d5bfbe 100644
--- a/packages/svelte/src/index-client.js
+++ b/packages/svelte/src/index-client.js
@@ -1,12 +1,48 @@
/** @import { ComponentContext, ComponentContextLegacy } from '#client' */
/** @import { EventDispatcher } from './index.js' */
/** @import { NotFunction } from './internal/types.js' */
-import { component_context, flush_sync, untrack } from './internal/client/runtime.js';
+import { flush_sync, untrack } from './internal/client/runtime.js';
import { is_array } from './internal/shared/utils.js';
import { user_effect } from './internal/client/index.js';
import * as e from './internal/client/errors.js';
import { lifecycle_outside_component } from './internal/shared/errors.js';
import { legacy_mode_flag } from './internal/flags/index.js';
+import { component_context } from './internal/client/context.js';
+import { DEV } from 'esm-env';
+
+if (DEV) {
+ /**
+ * @param {string} rune
+ */
+ function throw_rune_error(rune) {
+ if (!(rune in globalThis)) {
+ // TODO if people start adjusting the "this can contain runes" config through v-p-s more, adjust this message
+ /** @type {any} */
+ let value; // let's hope noone modifies this global, but belts and braces
+ Object.defineProperty(globalThis, rune, {
+ configurable: true,
+ // eslint-disable-next-line getter-return
+ get: () => {
+ if (value !== undefined) {
+ return value;
+ }
+
+ e.rune_outside_svelte(rune);
+ },
+ set: (v) => {
+ value = v;
+ }
+ });
+ }
+ }
+
+ throw_rune_error('$state');
+ throw_rune_error('$effect');
+ throw_rune_error('$derived');
+ throw_rune_error('$inspect');
+ throw_rune_error('$props');
+ throw_rune_error('$bindable');
+}
/**
* The `onMount` function schedules a callback to run as soon as the component has been mounted to the DOM.
@@ -179,15 +215,7 @@ export function flushSync(fn) {
flush_sync(fn);
}
+export { getContext, getAllContexts, hasContext, setContext } from './internal/client/context.js';
export { hydrate, mount, unmount } from './internal/client/render.js';
-
-export {
- getContext,
- getAllContexts,
- hasContext,
- setContext,
- tick,
- untrack
-} from './internal/client/runtime.js';
-
+export { tick, untrack } from './internal/client/runtime.js';
export { createRawSnippet } from './internal/client/dom/blocks/snippet.js';
diff --git a/packages/svelte/src/internal/client/context.js b/packages/svelte/src/internal/client/context.js
new file mode 100644
index 0000000000..bd94d5ad8a
--- /dev/null
+++ b/packages/svelte/src/internal/client/context.js
@@ -0,0 +1,211 @@
+/** @import { ComponentContext } from '#client' */
+
+import { DEV } from 'esm-env';
+import { add_owner } from './dev/ownership.js';
+import { lifecycle_outside_component } from '../shared/errors.js';
+import { source } from './reactivity/sources.js';
+import {
+ active_effect,
+ active_reaction,
+ set_active_effect,
+ set_active_reaction,
+ untrack
+} from './runtime.js';
+import { effect } from './reactivity/effects.js';
+import { legacy_mode_flag } from '../flags/index.js';
+
+/** @type {ComponentContext | null} */
+export let component_context = null;
+
+/** @param {ComponentContext | null} context */
+export function set_component_context(context) {
+ component_context = context;
+}
+
+/**
+ * The current component function. Different from current component context:
+ * ```html
+ *
+ *
+ *
+ *
+ * ```
+ * @type {ComponentContext['function']}
+ */
+export let dev_current_component_function = null;
+
+/** @param {ComponentContext['function']} fn */
+export function set_dev_current_component_function(fn) {
+ dev_current_component_function = fn;
+}
+
+/**
+ * Retrieves the context that belongs to the closest parent component with the specified `key`.
+ * Must be called during component initialisation.
+ *
+ * @template T
+ * @param {any} key
+ * @returns {T}
+ */
+export function getContext(key) {
+ const context_map = get_or_init_context_map('getContext');
+ const result = /** @type {T} */ (context_map.get(key));
+ return result;
+}
+
+/**
+ * Associates an arbitrary `context` object with the current component and the specified `key`
+ * and returns that object. The context is then available to children of the component
+ * (including slotted content) with `getContext`.
+ *
+ * Like lifecycle functions, this must be called during component initialisation.
+ *
+ * @template T
+ * @param {any} key
+ * @param {T} context
+ * @returns {T}
+ */
+export function setContext(key, context) {
+ const context_map = get_or_init_context_map('setContext');
+
+ if (DEV) {
+ // When state is put into context, we treat as if it's global from now on.
+ // We do for performance reasons (it's for example very expensive to call
+ // getContext on a big object many times when part of a list component)
+ // and danger of false positives.
+ untrack(() => add_owner(context, null, true));
+ }
+
+ context_map.set(key, context);
+ return context;
+}
+
+/**
+ * Checks whether a given `key` has been set in the context of a parent component.
+ * Must be called during component initialisation.
+ *
+ * @param {any} key
+ * @returns {boolean}
+ */
+export function hasContext(key) {
+ const context_map = get_or_init_context_map('hasContext');
+ return context_map.has(key);
+}
+
+/**
+ * Retrieves the whole context map that belongs to the closest parent component.
+ * Must be called during component initialisation. Useful, for example, if you
+ * programmatically create a component and want to pass the existing context to it.
+ *
+ * @template {Map} [T=Map]
+ * @returns {T}
+ */
+export function getAllContexts() {
+ const context_map = get_or_init_context_map('getAllContexts');
+ return /** @type {T} */ (context_map);
+}
+
+/**
+ * @param {Record} props
+ * @param {any} runes
+ * @param {Function} [fn]
+ * @returns {void}
+ */
+export function push(props, runes = false, fn) {
+ component_context = {
+ p: component_context,
+ c: null,
+ e: null,
+ m: false,
+ s: props,
+ x: null,
+ l: null
+ };
+
+ if (legacy_mode_flag && !runes) {
+ component_context.l = {
+ s: null,
+ u: null,
+ r1: [],
+ r2: source(false)
+ };
+ }
+
+ if (DEV) {
+ // component function
+ component_context.function = fn;
+ dev_current_component_function = fn;
+ }
+}
+
+/**
+ * @template {Record} T
+ * @param {T} [component]
+ * @returns {T}
+ */
+export function pop(component) {
+ const context_stack_item = component_context;
+ if (context_stack_item !== null) {
+ if (component !== undefined) {
+ context_stack_item.x = component;
+ }
+ const component_effects = context_stack_item.e;
+ if (component_effects !== null) {
+ var previous_effect = active_effect;
+ var previous_reaction = active_reaction;
+ context_stack_item.e = null;
+ try {
+ for (var i = 0; i < component_effects.length; i++) {
+ var component_effect = component_effects[i];
+ set_active_effect(component_effect.effect);
+ set_active_reaction(component_effect.reaction);
+ effect(component_effect.fn);
+ }
+ } finally {
+ set_active_effect(previous_effect);
+ set_active_reaction(previous_reaction);
+ }
+ }
+ component_context = context_stack_item.p;
+ if (DEV) {
+ dev_current_component_function = context_stack_item.p?.function ?? null;
+ }
+ context_stack_item.m = true;
+ }
+ // Micro-optimization: Don't set .a above to the empty object
+ // so it can be garbage-collected when the return here is unused
+ return component || /** @type {T} */ ({});
+}
+
+/** @returns {boolean} */
+export function is_runes() {
+ return !legacy_mode_flag || (component_context !== null && component_context.l === null);
+}
+
+/**
+ * @param {string} name
+ * @returns {Map}
+ */
+function get_or_init_context_map(name) {
+ if (component_context === null) {
+ lifecycle_outside_component(name);
+ }
+
+ return (component_context.c ??= new Map(get_parent_context(component_context) || undefined));
+}
+
+/**
+ * @param {ComponentContext} component_context
+ * @returns {Map | null}
+ */
+function get_parent_context(component_context) {
+ let parent = component_context.p;
+ while (parent !== null) {
+ const context_map = parent.c;
+ if (context_map !== null) {
+ return context_map;
+ }
+ parent = parent.p;
+ }
+ return null;
+}
diff --git a/packages/svelte/src/internal/client/dev/legacy.js b/packages/svelte/src/internal/client/dev/legacy.js
index a1d2fc8823..138213c551 100644
--- a/packages/svelte/src/internal/client/dev/legacy.js
+++ b/packages/svelte/src/internal/client/dev/legacy.js
@@ -1,5 +1,5 @@
import * as e from '../errors.js';
-import { component_context } from '../runtime.js';
+import { component_context } from '../context.js';
import { FILENAME } from '../../../constants.js';
import { get_component } from './ownership.js';
diff --git a/packages/svelte/src/internal/client/dev/ownership.js b/packages/svelte/src/internal/client/dev/ownership.js
index d113d9ae90..62119b36db 100644
--- a/packages/svelte/src/internal/client/dev/ownership.js
+++ b/packages/svelte/src/internal/client/dev/ownership.js
@@ -3,10 +3,10 @@
import { STATE_SYMBOL_METADATA } from '../constants.js';
import { render_effect, user_pre_effect } from '../reactivity/effects.js';
-import { dev_current_component_function } from '../runtime.js';
+import { dev_current_component_function } from '../context.js';
import { get_prototype_of } from '../../shared/utils.js';
import * as w from '../warnings.js';
-import { FILENAME } from '../../../constants.js';
+import { FILENAME, UNINITIALIZED } from '../../../constants.js';
/** @type {Record>} */
const boundaries = {};
@@ -109,7 +109,7 @@ export function mark_module_end(component) {
/**
* @param {any} object
- * @param {any} owner
+ * @param {any | null} owner
* @param {boolean} [global]
* @param {boolean} [skip_warning]
*/
@@ -120,7 +120,7 @@ export function add_owner(object, owner, global = false, skip_warning = false) {
if (metadata && !has_owner(metadata, component)) {
let original = get_owner(metadata);
- if (owner[FILENAME] !== component[FILENAME] && !skip_warning) {
+ if (owner && owner[FILENAME] !== component[FILENAME] && !skip_warning) {
w.ownership_invalid_binding(component[FILENAME], owner[FILENAME], original[FILENAME]);
}
}
@@ -140,6 +140,25 @@ export function add_owner_effect(get_object, Component, skip_warning = false) {
});
}
+/**
+ * @param {any} _this
+ * @param {Function} owner
+ * @param {Array<() => any>} getters
+ * @param {boolean} skip_warning
+ */
+export function add_owner_to_class(_this, owner, getters, skip_warning) {
+ _this[ADD_OWNER].current ||= getters.map(() => UNINITIALIZED);
+
+ for (let i = 0; i < getters.length; i += 1) {
+ const current = getters[i]();
+ // For performance reasons we only re-add the owner if the state has changed
+ if (current !== _this[ADD_OWNER][i]) {
+ _this[ADD_OWNER].current[i] = current;
+ add_owner(current, owner, false, skip_warning);
+ }
+ }
+}
+
/**
* @param {ProxyMetadata | null} from
* @param {ProxyMetadata} to
@@ -165,7 +184,7 @@ export function widen_ownership(from, to) {
/**
* @param {any} object
- * @param {Function} owner
+ * @param {Function | null} owner If `null`, then the object is globally owned and will not be checked
* @param {Set} seen
*/
function add_owner_to_object(object, owner, seen) {
@@ -174,7 +193,11 @@ function add_owner_to_object(object, owner, seen) {
if (metadata) {
// this is a state proxy, add owner directly, if not globally shared
if ('owners' in metadata && metadata.owners != null) {
- metadata.owners.add(owner);
+ if (owner) {
+ metadata.owners.add(owner);
+ } else {
+ metadata.owners = null;
+ }
}
} else if (object && typeof object === 'object') {
if (seen.has(object)) return;
@@ -192,7 +215,19 @@ function add_owner_to_object(object, owner, seen) {
if (proto === Object.prototype) {
// recurse until we find a state proxy
for (const key in object) {
- add_owner_to_object(object[key], owner, seen);
+ if (Object.getOwnPropertyDescriptor(object, key)?.get) {
+ // Similar to the class case; the getter could update with a new state
+ let current = UNINITIALIZED;
+ render_effect(() => {
+ const next = object[key];
+ if (current !== next) {
+ current = next;
+ add_owner_to_object(next, owner, seen);
+ }
+ });
+ } else {
+ add_owner_to_object(object[key], owner, seen);
+ }
}
} else if (proto === Array.prototype) {
// recurse until we find a state proxy
@@ -216,6 +251,11 @@ function has_owner(metadata, component) {
return (
metadata.owners.has(component) ||
+ // This helps avoid false positives when using HMR, where the component function is replaced
+ (FILENAME in component &&
+ [...metadata.owners].some(
+ (owner) => /** @type {any} */ (owner)[FILENAME] === component[FILENAME]
+ )) ||
(metadata.parent !== null && has_owner(metadata.parent, component))
);
}
diff --git a/packages/svelte/src/internal/client/dom/blocks/await.js b/packages/svelte/src/internal/client/dom/blocks/await.js
index 62b2e4dd0c..c8c7c1c0ea 100644
--- a/packages/svelte/src/internal/client/dom/blocks/await.js
+++ b/packages/svelte/src/internal/client/dom/blocks/await.js
@@ -3,18 +3,16 @@ import { DEV } from 'esm-env';
import { is_promise } from '../../../shared/utils.js';
import { block, branch, pause_effect, resume_effect } from '../../reactivity/effects.js';
import { internal_set, mutable_source, source } from '../../reactivity/sources.js';
+import { flush_sync, set_active_effect, set_active_reaction } from '../../runtime.js';
+import { hydrate_next, hydrate_node, hydrating } from '../hydration.js';
+import { queue_micro_task } from '../task.js';
+import { UNINITIALIZED } from '../../../../constants.js';
import {
component_context,
- flush_sync,
is_runes,
- set_active_effect,
- set_active_reaction,
set_component_context,
set_dev_current_component_function
-} from '../../runtime.js';
-import { hydrate_next, hydrate_node, hydrating } from '../hydration.js';
-import { queue_micro_task } from '../task.js';
-import { UNINITIALIZED } from '../../../../constants.js';
+} from '../../context.js';
const PENDING = 0;
const THEN = 1;
diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js
index 7f4f000dce..c1ca7a9600 100644
--- a/packages/svelte/src/internal/client/dom/blocks/boundary.js
+++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js
@@ -1,15 +1,14 @@
/** @import { Effect, TemplateNode, } from '#client' */
import { BOUNDARY_EFFECT, EFFECT_TRANSPARENT } from '../../constants.js';
+import { component_context, set_component_context } from '../../context.js';
import { block, branch, destroy_effect, pause_effect } from '../../reactivity/effects.js';
import {
active_effect,
active_reaction,
- component_context,
handle_error,
set_active_effect,
set_active_reaction,
- set_component_context,
reset_is_throwing_error
} from '../../runtime.js';
import {
diff --git a/packages/svelte/src/internal/client/dom/blocks/each.js b/packages/svelte/src/internal/client/dom/blocks/each.js
index b17090948a..3baa03a917 100644
--- a/packages/svelte/src/internal/client/dom/blocks/each.js
+++ b/packages/svelte/src/internal/client/dom/blocks/each.js
@@ -219,17 +219,7 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f
}
if (!hydrating) {
- var effect = /** @type {Effect} */ (active_reaction);
- reconcile(
- array,
- state,
- anchor,
- render_fn,
- flags,
- (effect.f & INERT) !== 0,
- get_key,
- get_collection
- );
+ reconcile(array, state, anchor, render_fn, flags, get_key, get_collection);
}
if (fallback_fn !== null) {
@@ -271,14 +261,13 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f
* @param {Array} array
* @param {EachState} state
* @param {Element | Comment | Text} anchor
- * @param {(anchor: Node, item: MaybeSource, index: number | Source) => void} render_fn
+ * @param {(anchor: Node, item: MaybeSource, index: number | Source, collection: () => V[]) => void} render_fn
* @param {number} flags
- * @param {boolean} is_inert
* @param {(value: V, index: number) => any} get_key
* @param {() => V[]} get_collection
* @returns {void}
*/
-function reconcile(array, state, anchor, render_fn, flags, is_inert, get_key, get_collection) {
+function reconcile(array, state, anchor, render_fn, flags, get_key, get_collection) {
var is_animated = (flags & EACH_IS_ANIMATED) !== 0;
var should_update = (flags & (EACH_ITEM_REACTIVE | EACH_INDEX_REACTIVE)) !== 0;
@@ -420,7 +409,7 @@ function reconcile(array, state, anchor, render_fn, flags, is_inert, get_key, ge
while (current !== null && current.k !== key) {
// If the each block isn't inert and an item has an effect that is already inert,
// skip over adding it to our seen Set as the item is already being handled
- if (is_inert || (current.e.f & INERT) === 0) {
+ if ((current.e.f & INERT) === 0) {
(seen ??= new Set()).add(current);
}
stashed.push(current);
@@ -444,7 +433,7 @@ function reconcile(array, state, anchor, render_fn, flags, is_inert, get_key, ge
while (current !== null) {
// If the each block isn't inert, then inert effects are currently outroing and will be removed once the transition is finished
- if (is_inert || (current.e.f & INERT) === 0) {
+ if ((current.e.f & INERT) === 0) {
to_destroy.push(current);
}
current = current.next;
@@ -510,7 +499,7 @@ function update_item(item, value, index, type) {
* @param {V} value
* @param {unknown} key
* @param {number} index
- * @param {(anchor: Node, item: V | Source, index: number | Value) => void} render_fn
+ * @param {(anchor: Node, item: V | Source, index: number | Value, collection: () => V[]) => void} render_fn
* @param {number} flags
* @param {() => V[]} get_collection
* @returns {EachItem}
@@ -559,7 +548,7 @@ function create_item(
current_each_item = item;
try {
- item.e = branch(() => render_fn(anchor, v, i), hydrating);
+ item.e = branch(() => render_fn(anchor, v, i, get_collection), hydrating);
item.e.prev = prev && prev.e;
item.e.next = next && next.e;
diff --git a/packages/svelte/src/internal/client/dom/blocks/html.js b/packages/svelte/src/internal/client/dom/blocks/html.js
index 04ab0aee87..b3fc5a9c72 100644
--- a/packages/svelte/src/internal/client/dom/blocks/html.js
+++ b/packages/svelte/src/internal/client/dom/blocks/html.js
@@ -7,7 +7,7 @@ import { assign_nodes } from '../template.js';
import * as w from '../../warnings.js';
import { hash, sanitize_location } from '../../../../utils.js';
import { DEV } from 'esm-env';
-import { dev_current_component_function } from '../../runtime.js';
+import { dev_current_component_function } from '../../context.js';
import { get_first_child, get_next_sibling } from '../operations.js';
/**
diff --git a/packages/svelte/src/internal/client/dom/blocks/key.js b/packages/svelte/src/internal/client/dom/blocks/key.js
index 4a8b7b94fc..a697163548 100644
--- a/packages/svelte/src/internal/client/dom/blocks/key.js
+++ b/packages/svelte/src/internal/client/dom/blocks/key.js
@@ -2,7 +2,7 @@
import { UNINITIALIZED } from '../../../../constants.js';
import { block, branch, pause_effect } from '../../reactivity/effects.js';
import { not_equal, safe_not_equal } from '../../reactivity/equality.js';
-import { is_runes } from '../../runtime.js';
+import { is_runes } from '../../context.js';
import { hydrate_next, hydrate_node, hydrating } from '../hydration.js';
/**
diff --git a/packages/svelte/src/internal/client/dom/blocks/snippet.js b/packages/svelte/src/internal/client/dom/blocks/snippet.js
index cec57f83b7..b916a02ce5 100644
--- a/packages/svelte/src/internal/client/dom/blocks/snippet.js
+++ b/packages/svelte/src/internal/client/dom/blocks/snippet.js
@@ -6,7 +6,7 @@ import { branch, block, destroy_effect, teardown } from '../../reactivity/effect
import {
dev_current_component_function,
set_dev_current_component_function
-} from '../../runtime.js';
+} from '../../context.js';
import { hydrate_next, hydrate_node, hydrating } from '../hydration.js';
import { create_fragment_from_html } from '../reconciler.js';
import { assign_nodes } from '../template.js';
diff --git a/packages/svelte/src/internal/client/dom/blocks/svelte-element.js b/packages/svelte/src/internal/client/dom/blocks/svelte-element.js
index 35d2f223ae..18641300e5 100644
--- a/packages/svelte/src/internal/client/dom/blocks/svelte-element.js
+++ b/packages/svelte/src/internal/client/dom/blocks/svelte-element.js
@@ -17,7 +17,8 @@ import {
} from '../../reactivity/effects.js';
import { set_should_intro } from '../../render.js';
import { current_each_item, set_current_each_item } from './each.js';
-import { component_context, active_effect } from '../../runtime.js';
+import { active_effect } from '../../runtime.js';
+import { component_context } from '../../context.js';
import { DEV } from 'esm-env';
import { EFFECT_TRANSPARENT } from '../../constants.js';
import { assign_nodes } from '../template.js';
diff --git a/packages/svelte/src/internal/client/dom/elements/attributes.js b/packages/svelte/src/internal/client/dom/elements/attributes.js
index a2fffe8696..2dba2d797a 100644
--- a/packages/svelte/src/internal/client/dom/elements/attributes.js
+++ b/packages/svelte/src/internal/client/dom/elements/attributes.js
@@ -1,5 +1,5 @@
import { DEV } from 'esm-env';
-import { hydrating } from '../hydration.js';
+import { hydrating, set_hydrating } from '../hydration.js';
import { get_descriptors, get_prototype_of } from '../../../shared/utils.js';
import { create_event, delegate } from './events.js';
import { add_form_reset_listener, autofocus } from './misc.js';
@@ -68,14 +68,14 @@ export function set_value(element, value) {
// treat null and undefined the same for the initial value
value ?? undefined) ||
// @ts-expect-error
- // `progress` elements always need their value set when its `0`
+ // `progress` elements always need their value set when it's `0`
(element.value === value && (value !== 0 || element.nodeName !== 'PROGRESS'))
) {
return;
}
// @ts-expect-error
- element.value = value;
+ element.value = value ?? '';
}
/**
@@ -213,6 +213,12 @@ export function set_custom_element_data(node, prop, value) {
// or effect
var previous_reaction = active_reaction;
var previous_effect = active_effect;
+ // If we're hydrating but the custom element is from Svelte, and it already scaffolded,
+ // then it might run block logic in hydration mode, which we have to prevent.
+ let was_hydrating = hydrating;
+ if (hydrating) {
+ set_hydrating(false);
+ }
set_active_reaction(null);
set_active_effect(null);
@@ -239,6 +245,9 @@ export function set_custom_element_data(node, prop, value) {
} finally {
set_active_reaction(previous_reaction);
set_active_effect(previous_effect);
+ if (was_hydrating) {
+ set_hydrating(true);
+ }
}
}
@@ -262,6 +271,13 @@ export function set_attributes(
is_custom_element = false,
skip_warning = false
) {
+ // If we're hydrating but the custom element is from Svelte, and it already scaffolded,
+ // then it might run block logic in hydration mode, which we have to prevent.
+ let is_hydrating_custom_element = hydrating && is_custom_element;
+ if (is_hydrating_custom_element) {
+ set_hydrating(false);
+ }
+
var current = prev || {};
var is_option_element = element.tagName === 'OPTION';
@@ -363,9 +379,10 @@ export function set_attributes(
element.style.cssText = value + '';
} else if (key === 'autofocus') {
autofocus(/** @type {HTMLElement} */ (element), Boolean(value));
- } else if (key === '__value' || (key === 'value' && value != null)) {
- // @ts-ignore
- element.value = element[key] = element.__value = value;
+ } else if (!is_custom_element && (key === '__value' || (key === 'value' && value != null))) {
+ // @ts-ignore We're not running this for custom elements because __value is actually
+ // how Lit stores the current value on the element, and messing with that would break things.
+ element.value = element.__value = value;
} else if (key === 'selected' && is_option_element) {
set_selected(/** @type {HTMLOptionElement} */ (element), value);
} else {
@@ -382,15 +399,18 @@ export function set_attributes(
if (name === 'value' || name === 'checked') {
// removing value/checked also removes defaultValue/defaultChecked — preserve
let input = /** @type {HTMLInputElement} */ (element);
-
+ const use_default = prev === undefined;
if (name === 'value') {
- let prev = input.defaultValue;
+ let previous = input.defaultValue;
input.removeAttribute(name);
- input.defaultValue = prev;
+ input.defaultValue = previous;
+ // @ts-ignore
+ input.value = input.__value = use_default ? previous : null;
} else {
- let prev = input.defaultChecked;
+ let previous = input.defaultChecked;
input.removeAttribute(name);
- input.defaultChecked = prev;
+ input.defaultChecked = previous;
+ input.checked = use_default ? previous : false;
}
} else {
element.removeAttribute(key);
@@ -402,11 +422,7 @@ export function set_attributes(
// @ts-ignore
element[name] = value;
} else if (typeof value !== 'function') {
- if (hydrating && (name === 'src' || name === 'href' || name === 'srcset')) {
- if (!skip_warning) check_src_in_dev_hydration(element, name, value ?? '');
- } else {
- set_attribute(element, name, value);
- }
+ set_attribute(element, name, value);
}
}
if (key === 'style' && '__styles' in element) {
@@ -415,6 +431,10 @@ export function set_attributes(
}
}
+ if (is_hydrating_custom_element) {
+ set_hydrating(true);
+ }
+
return current;
}
@@ -503,28 +523,3 @@ function srcset_url_equal(element, srcset) {
)
);
}
-
-/**
- * @param {HTMLImageElement} element
- * @returns {void}
- */
-export function handle_lazy_img(element) {
- // If we're using an image that has a lazy loading attribute, we need to apply
- // the loading and src after the img element has been appended to the document.
- // Otherwise the lazy behaviour will not work due to our cloneNode heuristic for
- // templates.
- if (!hydrating && element.loading === 'lazy') {
- var src = element.src;
- // @ts-expect-error
- element[LOADING_ATTR_SYMBOL] = null;
- element.loading = 'eager';
- element.removeAttribute('src');
- requestAnimationFrame(() => {
- // @ts-expect-error
- if (element[LOADING_ATTR_SYMBOL] !== 'eager') {
- element.loading = 'lazy';
- }
- element.src = src;
- });
- }
-}
diff --git a/packages/svelte/src/internal/client/dom/elements/bindings/input.js b/packages/svelte/src/internal/client/dom/elements/bindings/input.js
index 3ea1a24d7e..f1992007ed 100644
--- a/packages/svelte/src/internal/client/dom/elements/bindings/input.js
+++ b/packages/svelte/src/internal/client/dom/elements/bindings/input.js
@@ -5,7 +5,8 @@ import * as e from '../../../errors.js';
import { is } from '../../../proxy.js';
import { queue_micro_task } from '../../task.js';
import { hydrating } from '../../hydration.js';
-import { is_runes, untrack } from '../../../runtime.js';
+import { untrack } from '../../../runtime.js';
+import { is_runes } from '../../../context.js';
/**
* @param {HTMLInputElement} input
diff --git a/packages/svelte/src/internal/client/dom/elements/events.js b/packages/svelte/src/internal/client/dom/elements/events.js
index f2038f96ad..363b8e1ed5 100644
--- a/packages/svelte/src/internal/client/dom/elements/events.js
+++ b/packages/svelte/src/internal/client/dom/elements/events.js
@@ -49,10 +49,10 @@ export function replay_events(dom) {
/**
* @param {string} event_name
* @param {EventTarget} dom
- * @param {EventListener} handler
- * @param {AddEventListenerOptions} options
+ * @param {EventListener} [handler]
+ * @param {AddEventListenerOptions} [options]
*/
-export function create_event(event_name, dom, handler, options) {
+export function create_event(event_name, dom, handler, options = {}) {
/**
* @this {EventTarget}
*/
@@ -63,7 +63,7 @@ export function create_event(event_name, dom, handler, options) {
}
if (!event.cancelBubble) {
return without_reactive_context(() => {
- return handler.call(this, event);
+ return handler?.call(this, event);
});
}
}
@@ -108,8 +108,8 @@ export function on(element, type, handler, options = {}) {
/**
* @param {string} event_name
* @param {Element} dom
- * @param {EventListener} handler
- * @param {boolean} capture
+ * @param {EventListener} [handler]
+ * @param {boolean} [capture]
* @param {boolean} [passive]
* @returns {void}
*/
diff --git a/packages/svelte/src/internal/client/dom/legacy/lifecycle.js b/packages/svelte/src/internal/client/dom/legacy/lifecycle.js
index 5ffbacc670..a564712f04 100644
--- a/packages/svelte/src/internal/client/dom/legacy/lifecycle.js
+++ b/packages/svelte/src/internal/client/dom/legacy/lifecycle.js
@@ -1,8 +1,9 @@
/** @import { ComponentContextLegacy } from '#client' */
import { run, run_all } from '../../../shared/utils.js';
+import { component_context } from '../../context.js';
import { derived } from '../../reactivity/deriveds.js';
import { user_pre_effect, user_effect } from '../../reactivity/effects.js';
-import { component_context, deep_read_state, get, untrack } from '../../runtime.js';
+import { deep_read_state, get, untrack } from '../../runtime.js';
/**
* Legacy-mode only: Call `onMount` callbacks and set up `beforeUpdate`/`afterUpdate` effects
diff --git a/packages/svelte/src/internal/client/dom/operations.js b/packages/svelte/src/internal/client/dom/operations.js
index 627bf917ee..83565d17ae 100644
--- a/packages/svelte/src/internal/client/dom/operations.js
+++ b/packages/svelte/src/internal/client/dom/operations.js
@@ -11,6 +11,9 @@ export var $window;
/** @type {Document} */
export var $document;
+/** @type {boolean} */
+export var is_firefox;
+
/** @type {() => Node | null} */
var first_child_getter;
/** @type {() => Node | null} */
@@ -27,6 +30,7 @@ export function init_operations() {
$window = window;
$document = document;
+ is_firefox = /Firefox/.test(navigator.userAgent);
var element_prototype = Element.prototype;
var node_prototype = Node.prototype;
diff --git a/packages/svelte/src/internal/client/dom/template.js b/packages/svelte/src/internal/client/dom/template.js
index bcbae393ec..6ff3b0fa19 100644
--- a/packages/svelte/src/internal/client/dom/template.js
+++ b/packages/svelte/src/internal/client/dom/template.js
@@ -1,6 +1,6 @@
/** @import { Effect, TemplateNode } from '#client' */
import { hydrate_next, hydrate_node, hydrating, set_hydrate_node } from './hydration.js';
-import { create_text, get_first_child } from './operations.js';
+import { create_text, get_first_child, is_firefox } from './operations.js';
import { create_fragment_from_html } from './reconciler.js';
import { active_effect } from '../runtime.js';
import { TEMPLATE_FRAGMENT, TEMPLATE_USE_IMPORT_NODE } from '../../../constants.js';
@@ -48,7 +48,7 @@ export function template(content, flags) {
}
var clone = /** @type {TemplateNode} */ (
- use_import_node ? document.importNode(node, true) : node.cloneNode(true)
+ use_import_node || is_firefox ? document.importNode(node, true) : node.cloneNode(true)
);
if (is_fragment) {
@@ -249,3 +249,23 @@ export function append(anchor, dom) {
anchor.before(/** @type {Node} */ (dom));
}
+
+let uid = 1;
+
+/**
+ * Create (or hydrate) an unique UID for the component instance.
+ */
+export function props_id() {
+ if (
+ hydrating &&
+ hydrate_node &&
+ hydrate_node.nodeType === 8 &&
+ hydrate_node.textContent?.startsWith('#s')
+ ) {
+ const id = hydrate_node.textContent.substring(1);
+ hydrate_next();
+ return id;
+ }
+
+ return 'c' + uid++;
+}
diff --git a/packages/svelte/src/internal/client/index.js b/packages/svelte/src/internal/client/index.js
index 5b8969d5ee..e69f3c07d5 100644
--- a/packages/svelte/src/internal/client/index.js
+++ b/packages/svelte/src/internal/client/index.js
@@ -1,4 +1,5 @@
export { FILENAME, HMR, NAMESPACE_SVG } from '../../constants.js';
+export { push, pop } from './context.js';
export { assign, assign_and, assign_or, assign_nullish } from './dev/assign.js';
export { cleanup_styles } from './dev/css.js';
export { add_locations } from './dev/elements.js';
@@ -9,6 +10,7 @@ export {
mark_module_start,
mark_module_end,
add_owner_effect,
+ add_owner_to_class,
skip_ownership_validation
} from './dev/ownership.js';
export { check_target, legacy_api } from './dev/legacy.js';
@@ -33,7 +35,6 @@ export {
set_attributes,
set_custom_element_data,
set_xlink_attribute,
- handle_lazy_img,
set_value,
set_checked,
set_selected,
@@ -95,7 +96,8 @@ export {
mathml_template,
template,
template_with_script,
- text
+ text,
+ props_id
} from './dom/template.js';
export { derived, derived_safe_equal } from './reactivity/deriveds.js';
export {
@@ -115,6 +117,8 @@ export {
set,
simple_set,
state,
+ update,
+ update_pre,
get_options
} from './reactivity/sources.js';
export {
@@ -145,17 +149,9 @@ export {
flush_sync,
tick,
untrack,
- update,
- update_pre,
exclude_from_object,
- pop,
- push,
deep_read,
- deep_read_state,
- getAllContexts,
- getContext,
- setContext,
- hasContext
+ deep_read_state
} from './runtime.js';
export { validate_binding, validate_each_keys } from './validate.js';
export { raf } from './timing.js';
diff --git a/packages/svelte/src/internal/client/proxy.js b/packages/svelte/src/internal/client/proxy.js
index ae3fb3598d..caa6fadf1d 100644
--- a/packages/svelte/src/internal/client/proxy.js
+++ b/packages/svelte/src/internal/client/proxy.js
@@ -1,7 +1,8 @@
-/** @import { ProxyMetadata, ProxyStateObject, Source, ValueOptions } from '#client' */
+/** @import { ProxyMetadata, Source, ValueOptions } from '#client' */
import { DEV } from 'esm-env';
import { UNINITIALIZED } from '../../constants.js';
import { tracing_mode_flag } from '../flags/index.js';
+import { component_context } from './context.js';
import {
array_prototype,
get_descriptor,
@@ -14,7 +15,7 @@ import { check_ownership, widen_ownership } from './dev/ownership.js';
import { get_stack } from './dev/tracing.js';
import * as e from './errors.js';
import { batch_onchange, set, source, state } from './reactivity/sources.js';
-import { active_effect, component_context, get } from './runtime.js';
+import { active_effect, get } from './runtime.js';
const array_methods = ['push', 'pop', 'shift', 'unshift', 'splice', 'reverse', 'sort'];
diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js
index 7ec1ed30bd..59a7ed0f16 100644
--- a/packages/svelte/src/internal/client/reactivity/deriveds.js
+++ b/packages/svelte/src/internal/client/reactivity/deriveds.js
@@ -1,24 +1,14 @@
/** @import { Derived, Effect } from '#client' */
import { DEV } from 'esm-env';
-import {
- CLEAN,
- DERIVED,
- DESTROYED,
- DIRTY,
- EFFECT_HAS_DERIVED,
- MAYBE_DIRTY,
- UNOWNED
-} from '../constants.js';
+import { CLEAN, DERIVED, DIRTY, EFFECT_HAS_DERIVED, MAYBE_DIRTY, UNOWNED } from '../constants.js';
import {
active_reaction,
active_effect,
- remove_reactions,
set_signal_status,
skip_reaction,
update_reaction,
increment_write_version,
- set_active_effect,
- component_context
+ set_active_effect
} from '../runtime.js';
import { equals, safe_equals } from './equality.js';
import * as e from '../errors.js';
@@ -26,6 +16,7 @@ import { destroy_effect } from './effects.js';
import { inspect_effects, set_inspect_effects } from './sources.js';
import { get_stack } from '../dev/tracing.js';
import { tracing_mode_flag } from '../../flags/index.js';
+import { component_context } from '../context.js';
/**
* @template V
@@ -35,8 +26,12 @@ import { tracing_mode_flag } from '../../flags/index.js';
/*#__NO_SIDE_EFFECTS__*/
export function derived(fn) {
var flags = DERIVED | DIRTY;
+ var parent_derived =
+ active_reaction !== null && (active_reaction.f & DERIVED) !== 0
+ ? /** @type {Derived} */ (active_reaction)
+ : null;
- if (active_effect === null) {
+ if (active_effect === null || (parent_derived !== null && (parent_derived.f & UNOWNED) !== 0)) {
flags |= UNOWNED;
} else {
// Since deriveds are evaluated lazily, any effects created inside them are
@@ -44,16 +39,11 @@ export function derived(fn) {
active_effect.f |= EFFECT_HAS_DERIVED;
}
- var parent_derived =
- active_reaction !== null && (active_reaction.f & DERIVED) !== 0
- ? /** @type {Derived} */ (active_reaction)
- : null;
-
/** @type {Derived} */
const signal = {
- children: null,
ctx: component_context,
deps: null,
+ effects: null,
equals,
f: flags,
fn,
@@ -68,10 +58,6 @@ export function derived(fn) {
signal.created = get_stack('CreatedAt');
}
- if (parent_derived !== null) {
- (parent_derived.children ??= []).push(signal);
- }
-
return signal;
}
@@ -91,19 +77,14 @@ export function derived_safe_equal(fn) {
* @param {Derived} derived
* @returns {void}
*/
-function destroy_derived_children(derived) {
- var children = derived.children;
-
- if (children !== null) {
- derived.children = null;
-
- for (var i = 0; i < children.length; i += 1) {
- var child = children[i];
- if ((child.f & DERIVED) !== 0) {
- destroy_derived(/** @type {Derived} */ (child));
- } else {
- destroy_effect(/** @type {Effect} */ (child));
- }
+export function destroy_derived_effects(derived) {
+ var effects = derived.effects;
+
+ if (effects !== null) {
+ derived.effects = null;
+
+ for (var i = 0; i < effects.length; i += 1) {
+ destroy_effect(/** @type {Effect} */ (effects[i]));
}
}
}
@@ -151,7 +132,7 @@ export function execute_derived(derived) {
stack.push(derived);
- destroy_derived_children(derived);
+ destroy_derived_effects(derived);
value = update_reaction(derived);
} finally {
set_active_effect(prev_active_effect);
@@ -160,7 +141,7 @@ export function execute_derived(derived) {
}
} else {
try {
- destroy_derived_children(derived);
+ destroy_derived_effects(derived);
value = update_reaction(derived);
} finally {
set_active_effect(prev_active_effect);
@@ -186,15 +167,3 @@ export function update_derived(derived) {
derived.wv = increment_write_version();
}
}
-
-/**
- * @param {Derived} derived
- * @returns {void}
- */
-export function destroy_derived(derived) {
- destroy_derived_children(derived);
- remove_reactions(derived, 0);
- set_signal_status(derived, DESTROYED);
-
- derived.v = derived.children = derived.deps = derived.ctx = derived.reactions = null;
-}
diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js
index 1faf9a47a0..9d7b5e9de6 100644
--- a/packages/svelte/src/internal/client/reactivity/effects.js
+++ b/packages/svelte/src/internal/client/reactivity/effects.js
@@ -1,10 +1,8 @@
/** @import { ComponentContext, ComponentContextLegacy, Derived, Effect, TemplateNode, TransitionManager } from '#client' */
import {
check_dirtiness,
- component_context,
active_effect,
active_reaction,
- dev_current_component_function,
update_effect,
get,
is_destroying_effect,
@@ -44,7 +42,8 @@ import * as e from '../errors.js';
import { DEV } from 'esm-env';
import { define_property } from '../../shared/utils.js';
import { get_next_sibling } from '../dom/operations.js';
-import { derived, destroy_derived } from './deriveds.js';
+import { derived } from './deriveds.js';
+import { component_context, dev_current_component_function } from '../context.js';
/**
* @param {'$effect' | '$effect.pre' | '$inspect'} rune
@@ -54,7 +53,7 @@ export function validate_effect(rune) {
e.effect_orphan(rune);
}
- if (active_reaction !== null && (active_reaction.f & UNOWNED) !== 0) {
+ if (active_reaction !== null && (active_reaction.f & UNOWNED) !== 0 && active_effect === null) {
e.effect_in_unowned_derived();
}
@@ -100,7 +99,6 @@ function create_effect(type, fn, sync, push = true) {
var effect = {
ctx: component_context,
deps: null,
- deriveds: null,
nodes_start: null,
nodes_end: null,
f: type | DIRTY,
@@ -154,7 +152,7 @@ function create_effect(type, fn, sync, push = true) {
// 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.children ??= []).push(effect);
+ (derived.effects ??= []).push(effect);
}
}
@@ -166,13 +164,7 @@ function create_effect(type, fn, sync, push = true) {
* @returns {boolean}
*/
export function effect_tracking() {
- if (active_reaction === null || untracking) {
- return false;
- }
-
- // If it's skipped, that's because we're inside an unowned
- // that is not being tracked by another reaction
- return !skip_reaction;
+ return active_reaction !== null && !untracking;
}
/**
@@ -396,22 +388,6 @@ export function execute_effect_teardown(effect) {
}
}
-/**
- * @param {Effect} signal
- * @returns {void}
- */
-export function destroy_effect_deriveds(signal) {
- var deriveds = signal.deriveds;
-
- if (deriveds !== null) {
- signal.deriveds = null;
-
- for (var i = 0; i < deriveds.length; i += 1) {
- destroy_derived(deriveds[i]);
- }
- }
-}
-
/**
* @param {Effect} signal
* @param {boolean} remove_dom
@@ -469,7 +445,6 @@ export function destroy_effect(effect, remove_dom = true) {
}
destroy_effect_children(effect, remove_dom && !removed);
- destroy_effect_deriveds(effect);
remove_reactions(effect, 0);
set_signal_status(effect, DESTROYED);
diff --git a/packages/svelte/src/internal/client/reactivity/props.js b/packages/svelte/src/internal/client/reactivity/props.js
index 3e5a0258c7..5a3b30281f 100644
--- a/packages/svelte/src/internal/client/reactivity/props.js
+++ b/packages/svelte/src/internal/client/reactivity/props.js
@@ -8,7 +8,7 @@ import {
PROPS_IS_UPDATED
} from '../../../constants.js';
import { get_descriptor, is_function } from '../../shared/utils.js';
-import { mutable_source, set, source } from './sources.js';
+import { mutable_source, set, source, update } from './sources.js';
import { derived, derived_safe_equal } from './deriveds.js';
import {
active_effect,
@@ -16,7 +16,8 @@ import {
captured_signals,
set_active_effect,
untrack,
- update
+ active_reaction,
+ set_active_reaction
} from '../runtime.js';
import { safe_equals } from './equality.js';
import * as e from '../errors.js';
@@ -248,26 +249,6 @@ export function spread_props(...props) {
return new Proxy({ props }, spread_props_handler);
}
-/**
- * @template T
- * @param {() => T} fn
- * @returns {T}
- */
-function with_parent_branch(fn) {
- var effect = active_effect;
- var previous_effect = active_effect;
-
- while (effect !== null && (effect.f & (BRANCH_EFFECT | ROOT_EFFECT)) === 0) {
- effect = effect.parent;
- }
- try {
- set_active_effect(effect);
- return fn();
- } finally {
- set_active_effect(previous_effect);
- }
-}
-
/**
* This function is responsible for synchronizing a possibly bound prop with the inner component state.
* It is used whenever the compiler sees that the component writes to the prop, or when it has a default prop_value.
@@ -342,8 +323,8 @@ export function prop(props, key, flags, fallback) {
} else {
// Svelte 4 did not trigger updates when a primitive value was updated to the same value.
// Replicate that behavior through using a derived
- var derived_getter = with_parent_branch(() =>
- (immutable ? derived : derived_safe_equal)(() => /** @type {V} */ (props[key]))
+ var derived_getter = (immutable ? derived : derived_safe_equal)(
+ () => /** @type {V} */ (props[key])
);
derived_getter.f |= LEGACY_DERIVED_PROP;
getter = () => {
@@ -387,21 +368,19 @@ export function prop(props, key, flags, fallback) {
// The derived returns the current value. The underlying mutable
// source is written to from various places to persist this value.
var inner_current_value = mutable_source(prop_value);
- var current_value = with_parent_branch(() =>
- derived(() => {
- var parent_value = getter();
- var child_value = get(inner_current_value);
-
- if (from_child) {
- from_child = false;
- was_from_child = true;
- return child_value;
- }
+ var current_value = derived(() => {
+ var parent_value = getter();
+ var child_value = get(inner_current_value);
+
+ if (from_child) {
+ from_child = false;
+ was_from_child = true;
+ return child_value;
+ }
- was_from_child = false;
- return (inner_current_value.v = parent_value);
- })
- );
+ was_from_child = false;
+ return (inner_current_value.v = parent_value);
+ });
if (!immutable) current_value.equals = safe_equals;
diff --git a/packages/svelte/src/internal/client/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js
index 7495b7ef6e..ee550e4c4d 100644
--- a/packages/svelte/src/internal/client/reactivity/sources.js
+++ b/packages/svelte/src/internal/client/reactivity/sources.js
@@ -1,12 +1,10 @@
/** @import { Derived, Effect, Reaction, Source, Value, ValueOptions } from '#client' */
import { DEV } from 'esm-env';
import {
- component_context,
active_reaction,
active_effect,
untracked_writes,
get,
- is_runes,
schedule_effect,
set_untracked_writes,
set_signal_status,
@@ -36,6 +34,7 @@ import * as e from '../errors.js';
import { legacy_mode_flag, tracing_mode_flag } from '../../flags/index.js';
import { get_stack } from '../dev/tracing.js';
import { proxy } from '../proxy.js';
+import { component_context, is_runes } from '../context.js';
export let inspect_effects = new Set();
@@ -154,7 +153,7 @@ export function mutable_state(v, immutable = false) {
*/
/*#__NO_SIDE_EFFECTS__*/
function push_derived_source(source) {
- if (active_reaction !== null && (active_reaction.f & DERIVED) !== 0) {
+ if (active_reaction !== null && !untracking && (active_reaction.f & DERIVED) !== 0) {
if (derived_sources === null) {
set_derived_sources([source]);
} else {
@@ -303,6 +302,35 @@ export function internal_set(source, value) {
return value;
}
+/**
+ * @template {number | bigint} T
+ * @param {Source} source
+ * @param {1 | -1} [d]
+ * @returns {T}
+ */
+export function update(source, d = 1) {
+ var value = get(source);
+ var result = d === 1 ? value++ : value--;
+
+ set(source, value);
+
+ // @ts-expect-error
+ return result;
+}
+
+/**
+ * @template {number | bigint} T
+ * @param {Source} source
+ * @param {1 | -1} [d]
+ * @returns {T}
+ */
+export function update_pre(source, d = 1) {
+ var value = get(source);
+
+ // @ts-expect-error
+ return set(source, d === 1 ? ++value : --value);
+}
+
/**
* @param {Value} signal
* @param {number} status should be DIRTY or MAYBE_DIRTY
diff --git a/packages/svelte/src/internal/client/reactivity/types.d.ts b/packages/svelte/src/internal/client/reactivity/types.d.ts
index 0b1492e4dd..c0d490d5c7 100644
--- a/packages/svelte/src/internal/client/reactivity/types.d.ts
+++ b/packages/svelte/src/internal/client/reactivity/types.d.ts
@@ -42,8 +42,8 @@ export interface Reaction extends Signal {
export interface Derived extends Value, Reaction {
/** The derived function */
fn: () => V;
- /** Reactions created inside this signal */
- children: null | Reaction[];
+ /** Effects created inside this signal */
+ effects: null | Effect[];
/** Parent effect or derived */
parent: Effect | Derived | null;
}
@@ -57,8 +57,6 @@ export interface Effect extends Reaction {
*/
nodes_start: null | TemplateNode;
nodes_end: null | TemplateNode;
- /** Reactions created inside this signal */
- deriveds: null | Derived[];
/** The effect function */
fn: null | (() => void | (() => void));
/** The teardown function returned from the effect function */
diff --git a/packages/svelte/src/internal/client/render.js b/packages/svelte/src/internal/client/render.js
index 767b230131..3256fe8274 100644
--- a/packages/svelte/src/internal/client/render.js
+++ b/packages/svelte/src/internal/client/render.js
@@ -9,7 +9,8 @@ import {
init_operations
} from './dom/operations.js';
import { HYDRATION_END, HYDRATION_ERROR, HYDRATION_START } from '../../constants.js';
-import { push, pop, component_context, active_effect } from './runtime.js';
+import { active_effect } from './runtime.js';
+import { push, pop, component_context } from './context.js';
import { component_root, branch } from './reactivity/effects.js';
import {
hydrate_next,
@@ -54,7 +55,7 @@ export function set_text(text, value) {
if (str !== (text.__t ??= text.nodeValue)) {
// @ts-expect-error
text.__t = str;
- text.nodeValue = str == null ? '' : str + '';
+ text.nodeValue = str + '';
}
}
diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js
index 1d695e1fee..75d45d9db9 100644
--- a/packages/svelte/src/internal/client/runtime.js
+++ b/packages/svelte/src/internal/client/runtime.js
@@ -4,8 +4,6 @@ import { define_property, get_descriptors, get_prototype_of, index_of } from '..
import {
destroy_block_effect_children,
destroy_effect_children,
- destroy_effect_deriveds,
- effect,
execute_effect_teardown,
unlink_effect
} from './reactivity/effects.js';
@@ -28,14 +26,20 @@ import {
BOUNDARY_EFFECT
} from './constants.js';
import { flush_tasks } from './dom/task.js';
-import { add_owner } from './dev/ownership.js';
-import { internal_set, set, source } from './reactivity/sources.js';
-import { destroy_derived, execute_derived, update_derived } from './reactivity/deriveds.js';
+import { internal_set } from './reactivity/sources.js';
+import { destroy_derived_effects, update_derived } from './reactivity/deriveds.js';
import * as e from './errors.js';
-import { lifecycle_outside_component } from '../shared/errors.js';
import { FILENAME } from '../../constants.js';
-import { legacy_mode_flag, tracing_mode_flag } from '../flags/index.js';
+import { tracing_mode_flag } from '../flags/index.js';
import { tracing_expressions, get_stack } from './dev/tracing.js';
+import {
+ component_context,
+ dev_current_component_function,
+ is_runes,
+ set_component_context,
+ set_dev_current_component_function
+} from './context.js';
+import { is_firefox } from './dom/operations.js';
const FLUSH_MICROTASK = 0;
const FLUSH_SYNC = 1;
@@ -150,41 +154,10 @@ export function set_captured_signals(value) {
captured_signals = value;
}
-// Handling runtime component context
-/** @type {ComponentContext | null} */
-export let component_context = null;
-
-/** @param {ComponentContext | null} context */
-export function set_component_context(context) {
- component_context = context;
-}
-
-/**
- * The current component function. Different from current component context:
- * ```html
- *
- *
- *
- *
- * ```
- * @type {ComponentContext['function']}
- */
-export let dev_current_component_function = null;
-
-/** @param {ComponentContext['function']} fn */
-export function set_dev_current_component_function(fn) {
- dev_current_component_function = fn;
-}
-
export function increment_write_version() {
return ++write_version;
}
-/** @returns {boolean} */
-export function is_runes() {
- return !legacy_mode_flag || (component_context !== null && component_context.l === null);
-}
-
/**
* Determines whether a derived or effect is dirty.
* If it is MAYBE_DIRTY, will set the status to CLEAN
@@ -212,18 +185,28 @@ export function check_dirtiness(reaction) {
// If we are working with a disconnected or an unowned signal that is now connected (due to an active effect)
// then we need to re-connect the reaction to the dependency
if (is_disconnected || is_unowned_connected) {
+ var derived = /** @type {Derived} */ (reaction);
+ var parent = derived.parent;
+
for (i = 0; i < length; i++) {
dependency = dependencies[i];
// We always re-add all reactions (even duplicates) if the derived was
- // previously disconnected
- if (is_disconnected || !dependency?.reactions?.includes(reaction)) {
- (dependency.reactions ??= []).push(reaction);
+ // previously disconnected, however we don't if it was unowned as we
+ // de-duplicate dependencies in that case
+ if (is_disconnected || !dependency?.reactions?.includes(derived)) {
+ (dependency.reactions ??= []).push(derived);
}
}
if (is_disconnected) {
- reaction.f ^= DISCONNECTED;
+ derived.f ^= DISCONNECTED;
+ }
+ // If the unowned derived is now fully connected to the graph again (it's unowned and reconnected, has a parent
+ // and the parent is not unowned), then we can mark it as connected again, removing the need for the unowned
+ // flag
+ if (is_unowned_connected && parent !== null && (parent.f & UNOWNED) === 0) {
+ derived.f ^= UNOWNED;
}
}
@@ -351,7 +334,7 @@ export function handle_error(error, effect, previous_effect, component_context)
current_context = current_context.p;
}
- const indent = /Firefox/.test(navigator.userAgent) ? ' ' : '\t';
+ const indent = is_firefox ? ' ' : '\t';
define_property(error, 'message', {
value: error.message + `\n${component_stack.map((name) => `\n${indent}in ${name}`).join('')}\n`
});
@@ -432,9 +415,12 @@ export function update_reaction(reaction) {
skipped_deps = 0;
untracked_writes = null;
active_reaction = (flags & (BRANCH_EFFECT | ROOT_EFFECT)) === 0 ? reaction : null;
- skip_reaction = !is_flushing_effect && (flags & UNOWNED) !== 0;
+ skip_reaction =
+ (flags & UNOWNED) !== 0 &&
+ (!is_flushing_effect || previous_reaction === null || previous_untracking);
+
derived_sources = null;
- component_context = reaction.ctx;
+ set_component_context(reaction.ctx);
untracking = false;
read_version++;
@@ -498,7 +484,7 @@ export function update_reaction(reaction) {
active_reaction = previous_reaction;
skip_reaction = previous_skip_reaction;
derived_sources = prev_derived_sources;
- component_context = previous_component_context;
+ set_component_context(previous_component_context);
untracking = previous_untracking;
}
}
@@ -540,6 +526,8 @@ function remove_reaction(signal, dependency) {
if ((dependency.f & (UNOWNED | DISCONNECTED)) === 0) {
dependency.f ^= DISCONNECTED;
}
+ // Disconnect any reactions owned by this reaction
+ destroy_derived_effects(/** @type {Derived} **/ (dependency));
remove_reactions(/** @type {Derived} **/ (dependency), 0);
}
}
@@ -578,7 +566,7 @@ export function update_effect(effect) {
if (DEV) {
var previous_component_fn = dev_current_component_function;
- dev_current_component_function = effect.component_function;
+ set_dev_current_component_function(effect.component_function);
}
try {
@@ -587,7 +575,6 @@ export function update_effect(effect) {
} else {
destroy_effect_children(effect);
}
- destroy_effect_deriveds(effect);
execute_effect_teardown(effect);
var teardown = update_reaction(effect);
@@ -620,7 +607,7 @@ export function update_effect(effect) {
active_effect = previous_effect;
if (DEV) {
- dev_current_component_function = previous_component_fn;
+ set_dev_current_component_function(previous_component_fn);
}
}
}
@@ -693,10 +680,7 @@ function flush_queued_root_effects(root_effects) {
effect.f ^= CLEAN;
}
- /** @type {Effect[]} */
- var collected_effects = [];
-
- process_effects(effect, collected_effects);
+ var collected_effects = process_effects(effect);
flush_queued_effects(collected_effects);
}
} finally {
@@ -797,13 +781,14 @@ export function schedule_effect(signal) {
* effects to be flushed.
*
* @param {Effect} effect
- * @param {Effect[]} collected_effects
- * @returns {void}
+ * @returns {Effect[]}
*/
-function process_effects(effect, collected_effects) {
- var current_effect = effect.first;
+function process_effects(effect) {
+ /** @type {Effect[]} */
var effects = [];
+ var current_effect = effect.first;
+
main_loop: while (current_effect !== null) {
var flags = current_effect.f;
var is_branch = (flags & BRANCH_EFFECT) !== 0;
@@ -811,27 +796,32 @@ function process_effects(effect, collected_effects) {
var sibling = current_effect.next;
if (!is_skippable_branch && (flags & INERT) === 0) {
- if ((flags & RENDER_EFFECT) !== 0) {
- if (is_branch) {
- current_effect.f ^= CLEAN;
- } else {
- try {
- if (check_dirtiness(current_effect)) {
- update_effect(current_effect);
- }
- } catch (error) {
- handle_error(error, current_effect, null, current_effect.ctx);
+ if ((flags & EFFECT) !== 0) {
+ effects.push(current_effect);
+ } else if (is_branch) {
+ current_effect.f ^= CLEAN;
+ } else {
+ // Ensure we set the effect to be the active reaction
+ // to ensure that unowned deriveds are correctly tracked
+ // because we're flushing the current effect
+ var previous_active_reaction = active_reaction;
+ try {
+ active_reaction = current_effect;
+ if (check_dirtiness(current_effect)) {
+ update_effect(current_effect);
}
+ } catch (error) {
+ handle_error(error, current_effect, null, current_effect.ctx);
+ } finally {
+ active_reaction = previous_active_reaction;
}
+ }
- var child = current_effect.first;
+ var child = current_effect.first;
- if (child !== null) {
- current_effect = child;
- continue;
- }
- } else if ((flags & EFFECT) !== 0) {
- effects.push(current_effect);
+ if (child !== null) {
+ current_effect = child;
+ continue;
}
}
@@ -854,13 +844,7 @@ function process_effects(effect, collected_effects) {
current_effect = sibling;
}
- // We might be dealing with many effects here, far more than can be spread into
- // an array push call (callstack overflow). So let's deal with each effect in a loop.
- for (var i = 0; i < effects.length; i++) {
- child = effects[i];
- collected_effects.push(child);
- process_effects(child, collected_effects);
- }
+ return effects;
}
/**
@@ -925,15 +909,6 @@ export function get(signal) {
var flags = signal.f;
var is_derived = (flags & DERIVED) !== 0;
- // If the derived is destroyed, just execute it again without retaining
- // its memoisation properties as the derived is stale
- if (is_derived && (flags & DESTROYED) !== 0) {
- var value = execute_derived(/** @type {Derived} */ (signal));
- // Ensure the derived remains destroyed
- destroy_derived(/** @type {Derived} */ (signal));
- return value;
- }
-
if (captured_signals !== null) {
captured_signals.add(signal);
}
@@ -953,31 +928,26 @@ export function get(signal) {
skipped_deps++;
} else if (new_deps === null) {
new_deps = [signal];
- } else {
+ } else if (!skip_reaction || !new_deps.includes(signal)) {
+ // Normally we can push duplicated dependencies to `new_deps`, but if we're inside
+ // an unowned derived because skip_reaction is true, then we need to ensure that
+ // we don't have duplicates
new_deps.push(signal);
}
}
- } else if (is_derived && /** @type {Derived} */ (signal).deps === null) {
+ } else if (
+ is_derived &&
+ /** @type {Derived} */ (signal).deps === null &&
+ /** @type {Derived} */ (signal).effects === null
+ ) {
var derived = /** @type {Derived} */ (signal);
var parent = derived.parent;
- var target = derived;
- while (parent !== null) {
- // Attach the derived to the nearest parent effect, if there are deriveds
- // in between then we also need to attach them too
- if ((parent.f & DERIVED) !== 0) {
- var parent_derived = /** @type {Derived} */ (parent);
-
- target = parent_derived;
- parent = parent_derived.parent;
- } else {
- var parent_effect = /** @type {Effect} */ (parent);
-
- if (!parent_effect.deriveds?.includes(target)) {
- (parent_effect.deriveds ??= []).push(target);
- }
- break;
- }
+ if (parent !== null && (parent.f & UNOWNED) === 0) {
+ // If the derived is owned by another derived then mark it as unowned
+ // as the derived value might have been referenced in a different context
+ // since and thus its parent might not be its true owner anymore
+ derived.f ^= UNOWNED;
}
}
@@ -1110,138 +1080,6 @@ export function set_signal_status(signal, status) {
signal.f = (signal.f & STATUS_MASK) | status;
}
-/**
- * Retrieves the context that belongs to the closest parent component with the specified `key`.
- * Must be called during component initialisation.
- *
- * @template T
- * @param {any} key
- * @returns {T}
- */
-export function getContext(key) {
- const context_map = get_or_init_context_map('getContext');
- const result = /** @type {T} */ (context_map.get(key));
-
- if (DEV) {
- const fn = /** @type {ComponentContext} */ (component_context).function;
- if (fn) {
- add_owner(result, fn, true);
- }
- }
-
- return result;
-}
-
-/**
- * Associates an arbitrary `context` object with the current component and the specified `key`
- * and returns that object. The context is then available to children of the component
- * (including slotted content) with `getContext`.
- *
- * Like lifecycle functions, this must be called during component initialisation.
- *
- * @template T
- * @param {any} key
- * @param {T} context
- * @returns {T}
- */
-export function setContext(key, context) {
- const context_map = get_or_init_context_map('setContext');
- context_map.set(key, context);
- return context;
-}
-
-/**
- * Checks whether a given `key` has been set in the context of a parent component.
- * Must be called during component initialisation.
- *
- * @param {any} key
- * @returns {boolean}
- */
-export function hasContext(key) {
- const context_map = get_or_init_context_map('hasContext');
- return context_map.has(key);
-}
-
-/**
- * Retrieves the whole context map that belongs to the closest parent component.
- * Must be called during component initialisation. Useful, for example, if you
- * programmatically create a component and want to pass the existing context to it.
- *
- * @template {Map} [T=Map]
- * @returns {T}
- */
-export function getAllContexts() {
- const context_map = get_or_init_context_map('getAllContexts');
-
- if (DEV) {
- const fn = component_context?.function;
- if (fn) {
- for (const value of context_map.values()) {
- add_owner(value, fn, true);
- }
- }
- }
-
- return /** @type {T} */ (context_map);
-}
-
-/**
- * @param {string} name
- * @returns {Map}
- */
-function get_or_init_context_map(name) {
- if (component_context === null) {
- lifecycle_outside_component(name);
- }
-
- return (component_context.c ??= new Map(get_parent_context(component_context) || undefined));
-}
-
-/**
- * @param {ComponentContext} component_context
- * @returns {Map | null}
- */
-function get_parent_context(component_context) {
- let parent = component_context.p;
- while (parent !== null) {
- const context_map = parent.c;
- if (context_map !== null) {
- return context_map;
- }
- parent = parent.p;
- }
- return null;
-}
-
-/**
- * @template {number | bigint} T
- * @param {Value} signal
- * @param {1 | -1} [d]
- * @returns {T}
- */
-export function update(signal, d = 1) {
- var value = get(signal);
- var result = d === 1 ? value++ : value--;
-
- set(signal, value);
-
- // @ts-expect-error
- return result;
-}
-
-/**
- * @template {number | bigint} T
- * @param {Value} signal
- * @param {1 | -1} [d]
- * @returns {T}
- */
-export function update_pre(signal, d = 1) {
- var value = get(signal);
-
- // @ts-expect-error
- return set(signal, d === 1 ? ++value : --value);
-}
-
/**
* @param {Record} obj
* @param {string[]} keys
@@ -1260,78 +1098,6 @@ export function exclude_from_object(obj, keys) {
return result;
}
-/**
- * @param {Record} props
- * @param {any} runes
- * @param {Function} [fn]
- * @returns {void}
- */
-export function push(props, runes = false, fn) {
- component_context = {
- p: component_context,
- c: null,
- e: null,
- m: false,
- s: props,
- x: null,
- l: null
- };
-
- if (legacy_mode_flag && !runes) {
- component_context.l = {
- s: null,
- u: null,
- r1: [],
- r2: source(false)
- };
- }
-
- if (DEV) {
- // component function
- component_context.function = fn;
- dev_current_component_function = fn;
- }
-}
-
-/**
- * @template {Record} T
- * @param {T} [component]
- * @returns {T}
- */
-export function pop(component) {
- const context_stack_item = component_context;
- if (context_stack_item !== null) {
- if (component !== undefined) {
- context_stack_item.x = component;
- }
- const component_effects = context_stack_item.e;
- if (component_effects !== null) {
- var previous_effect = active_effect;
- var previous_reaction = active_reaction;
- context_stack_item.e = null;
- try {
- for (var i = 0; i < component_effects.length; i++) {
- var component_effect = component_effects[i];
- set_active_effect(component_effect.effect);
- set_active_reaction(component_effect.reaction);
- effect(component_effect.fn);
- }
- } finally {
- set_active_effect(previous_effect);
- set_active_reaction(previous_reaction);
- }
- }
- component_context = context_stack_item.p;
- if (DEV) {
- dev_current_component_function = context_stack_item.p?.function ?? null;
- }
- context_stack_item.m = true;
- }
- // Micro-optimization: Don't set .a above to the empty object
- // so it can be garbage-collected when the return here is unused
- return component || /** @type {T} */ ({});
-}
-
/**
* Possibly traverse an object and read all its properties so that they're all reactive in case this is `$state`.
* Does only check first level of an object for performance reasons (heuristic should be good for 99% of all cases).
@@ -1405,37 +1171,3 @@ export function deep_read(value, visited = new Set()) {
}
}
}
-
-if (DEV) {
- /**
- * @param {string} rune
- */
- function throw_rune_error(rune) {
- if (!(rune in globalThis)) {
- // TODO if people start adjusting the "this can contain runes" config through v-p-s more, adjust this message
- /** @type {any} */
- let value; // let's hope noone modifies this global, but belts and braces
- Object.defineProperty(globalThis, rune, {
- configurable: true,
- // eslint-disable-next-line getter-return
- get: () => {
- if (value !== undefined) {
- return value;
- }
-
- e.rune_outside_svelte(rune);
- },
- set: (v) => {
- value = v;
- }
- });
- }
- }
-
- throw_rune_error('$state');
- throw_rune_error('$effect');
- throw_rune_error('$derived');
- throw_rune_error('$inspect');
- throw_rune_error('$props');
- throw_rune_error('$bindable');
-}
diff --git a/packages/svelte/src/internal/client/validate.js b/packages/svelte/src/internal/client/validate.js
index 24e280edf8..ec3d805447 100644
--- a/packages/svelte/src/internal/client/validate.js
+++ b/packages/svelte/src/internal/client/validate.js
@@ -1,4 +1,4 @@
-import { dev_current_component_function } from './runtime.js';
+import { dev_current_component_function } from './context.js';
import { is_array } from '../shared/utils.js';
import * as e from './errors.js';
import { FILENAME } from '../../constants.js';
diff --git a/packages/svelte/src/internal/server/index.js b/packages/svelte/src/internal/server/index.js
index 89b3c33df8..c4e5d318dc 100644
--- a/packages/svelte/src/internal/server/index.js
+++ b/packages/svelte/src/internal/server/index.js
@@ -28,14 +28,15 @@ const INVALID_ATTR_NAME_CHAR_REGEX =
* @param {Payload} to_copy
* @returns {Payload}
*/
-export function copy_payload({ out, css, head }) {
+export function copy_payload({ out, css, head, uid }) {
return {
out,
css: new Set(css),
head: {
title: head.title,
out: head.out
- }
+ },
+ uid
};
}
@@ -48,6 +49,7 @@ export function copy_payload({ out, css, head }) {
export function assign_payload(p1, p2) {
p1.out = p2.out;
p1.head = p2.head;
+ p1.uid = p2.uid;
}
/**
@@ -83,17 +85,27 @@ export function element(payload, tag, attributes_fn = noop, children_fn = noop)
*/
export let on_destroy = [];
+function props_id_generator() {
+ let uid = 1;
+ return () => 's' + uid++;
+}
+
/**
* Only available on the server and when compiling with the `server` option.
* Takes a component and returns an object with `body` and `head` properties on it, which you can use to populate the HTML when server-rendering your app.
* @template {Record} Props
* @param {import('svelte').Component | ComponentType>} component
- * @param {{ props?: Omit; context?: Map }} [options]
+ * @param {{ props?: Omit; context?: Map, uid?: () => string }} [options]
* @returns {RenderOutput}
*/
export function render(component, options = {}) {
/** @type {Payload} */
- const payload = { out: '', css: new Set(), head: { title: '', out: '' } };
+ const payload = {
+ out: '',
+ css: new Set(),
+ head: { title: '', out: '' },
+ uid: options.uid ?? props_id_generator()
+ };
const prev_on_destroy = on_destroy;
on_destroy = [];
@@ -526,6 +538,17 @@ export function once(get_value) {
};
}
+/**
+ * Create an unique ID
+ * @param {Payload} payload
+ * @returns {string}
+ */
+export function props_id(payload) {
+ const uid = payload.uid();
+ payload.out += '';
+ return uid;
+}
+
export { attr, clsx };
export { html } from './blocks/html.js';
diff --git a/packages/svelte/src/internal/server/types.d.ts b/packages/svelte/src/internal/server/types.d.ts
index e6c235147b..8a241deecd 100644
--- a/packages/svelte/src/internal/server/types.d.ts
+++ b/packages/svelte/src/internal/server/types.d.ts
@@ -18,6 +18,8 @@ export interface Payload {
title: string;
out: string;
};
+ /** Function that generates a unique ID */
+ uid: () => string;
}
export interface RenderOutput {
diff --git a/packages/svelte/src/legacy/legacy-client.js b/packages/svelte/src/legacy/legacy-client.js
index 3715617f4c..3a05bc0496 100644
--- a/packages/svelte/src/legacy/legacy-client.js
+++ b/packages/svelte/src/legacy/legacy-client.js
@@ -3,19 +3,13 @@ import { DIRTY, LEGACY_PROPS, MAYBE_DIRTY } from '../internal/client/constants.j
import { user_pre_effect } from '../internal/client/reactivity/effects.js';
import { mutable_source, set } from '../internal/client/reactivity/sources.js';
import { hydrate, mount, unmount } from '../internal/client/render.js';
-import {
- active_effect,
- component_context,
- dev_current_component_function,
- flush_sync,
- get,
- set_signal_status
-} from '../internal/client/runtime.js';
+import { active_effect, flush_sync, get, set_signal_status } from '../internal/client/runtime.js';
import { lifecycle_outside_component } from '../internal/shared/errors.js';
import { define_property, is_array } from '../internal/shared/utils.js';
import * as w from '../internal/client/warnings.js';
import { DEV } from 'esm-env';
import { FILENAME } from '../constants.js';
+import { component_context, dev_current_component_function } from '../internal/client/context.js';
/**
* Takes the same options as a Svelte 4 component and the component function and returns a Svelte 4 compatible component.
diff --git a/packages/svelte/src/utils.js b/packages/svelte/src/utils.js
index 76486d32ac..d4d106d56d 100644
--- a/packages/svelte/src/utils.js
+++ b/packages/svelte/src/utils.js
@@ -170,7 +170,10 @@ const DOM_BOOLEAN_ATTRIBUTES = [
'reversed',
'seamless',
'selected',
- 'webkitdirectory'
+ 'webkitdirectory',
+ 'defer',
+ 'disablepictureinpicture',
+ 'disableremoteplayback'
];
/**
@@ -196,7 +199,11 @@ const ATTRIBUTE_ALIASES = {
readonly: 'readOnly',
defaultvalue: 'defaultValue',
defaultchecked: 'defaultChecked',
- srcobject: 'srcObject'
+ srcobject: 'srcObject',
+ novalidate: 'noValidate',
+ allowfullscreen: 'allowFullscreen',
+ disablepictureinpicture: 'disablePictureInPicture',
+ disableremoteplayback: 'disableRemotePlayback'
};
/**
@@ -218,7 +225,11 @@ const DOM_PROPERTIES = [
'volume',
'defaultValue',
'defaultChecked',
- 'srcObject'
+ 'srcObject',
+ 'noValidate',
+ 'allowFullscreen',
+ 'disablePictureInPicture',
+ 'disableRemotePlayback'
];
/**
@@ -422,6 +433,7 @@ const RUNES = /** @type {const} */ ([
'$state.raw',
'$state.snapshot',
'$props',
+ '$props.id',
'$bindable',
'$derived',
'$derived.by',
diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js
index b0ea99b6f4..ada6f9019d 100644
--- a/packages/svelte/src/version.js
+++ b/packages/svelte/src/version.js
@@ -4,5 +4,5 @@
* The current version, as set in package.json.
* @type {string}
*/
-export const VERSION = '5.19.1';
+export const VERSION = '5.19.10';
export const PUBLIC_VERSION = '5';
diff --git a/packages/svelte/tests/css/samples/general-siblings-combinator-svelteelement/_config.js b/packages/svelte/tests/css/samples/general-siblings-combinator-svelteelement/_config.js
index 7c50a76bbb..a7d6c3a99d 100644
--- a/packages/svelte/tests/css/samples/general-siblings-combinator-svelteelement/_config.js
+++ b/packages/svelte/tests/css/samples/general-siblings-combinator-svelteelement/_config.js
@@ -5,15 +5,15 @@ export default test({
{
code: 'css_unused_selector',
end: {
- character: 496,
+ character: 627,
column: 10,
- line: 26
+ line: 32
},
message: 'Unused CSS selector ".x + .bar"',
start: {
- character: 487,
+ character: 618,
column: 1,
- line: 26
+ line: 32
}
}
]
diff --git a/packages/svelte/tests/css/samples/general-siblings-combinator-svelteelement/expected.css b/packages/svelte/tests/css/samples/general-siblings-combinator-svelteelement/expected.css
index 830d366702..d3fa8c97d2 100644
--- a/packages/svelte/tests/css/samples/general-siblings-combinator-svelteelement/expected.css
+++ b/packages/svelte/tests/css/samples/general-siblings-combinator-svelteelement/expected.css
@@ -9,5 +9,8 @@
.x.svelte-xyz ~ .foo:where(.svelte-xyz) span:where(.svelte-xyz) { color: green; }
.x.svelte-xyz ~ .bar:where(.svelte-xyz) { color: green; }
+ .z.svelte-xyz + .z:where(.svelte-xyz) { color: green; }
+ .z.svelte-xyz ~ .z:where(.svelte-xyz) { color: green; }
+
/* no match */
/* (unused) .x + .bar { color: green; }*/
diff --git a/packages/svelte/tests/css/samples/general-siblings-combinator-svelteelement/input.svelte b/packages/svelte/tests/css/samples/general-siblings-combinator-svelteelement/input.svelte
index 1c51a2c516..655fd86153 100644
--- a/packages/svelte/tests/css/samples/general-siblings-combinator-svelteelement/input.svelte
+++ b/packages/svelte/tests/css/samples/general-siblings-combinator-svelteelement/input.svelte
@@ -10,6 +10,9 @@
bar
+{#each [1]}
+
+{/each}
diff --git a/packages/svelte/tests/css/samples/has/_config.js b/packages/svelte/tests/css/samples/has/_config.js
index e5dc5f3459..8d89d98cbd 100644
--- a/packages/svelte/tests/css/samples/has/_config.js
+++ b/packages/svelte/tests/css/samples/has/_config.js
@@ -6,182 +6,210 @@ export default test({
code: 'css_unused_selector',
message: 'Unused CSS selector ".unused:has(y)"',
start: {
- line: 31,
+ line: 33,
column: 1,
- character: 308
+ character: 330
},
end: {
- line: 31,
+ line: 33,
column: 15,
- character: 322
+ character: 344
}
},
{
code: 'css_unused_selector',
message: 'Unused CSS selector ".unused:has(:global(y))"',
start: {
- line: 34,
+ line: 36,
column: 1,
- character: 343
+ character: 365
},
end: {
- line: 34,
+ line: 36,
column: 24,
- character: 366
+ character: 388
}
},
{
code: 'css_unused_selector',
message: 'Unused CSS selector "x:has(.unused)"',
start: {
- line: 37,
+ line: 39,
column: 1,
- character: 387
+ character: 409
},
end: {
- line: 37,
+ line: 39,
column: 15,
- character: 401
+ character: 423
}
},
{
code: 'css_unused_selector',
message: 'Unused CSS selector ":global(.foo):has(.unused)"',
start: {
- line: 40,
+ line: 42,
column: 1,
- character: 422
+ character: 444
},
end: {
- line: 40,
+ line: 42,
column: 27,
- character: 448
+ character: 470
}
},
{
code: 'css_unused_selector',
message: 'Unused CSS selector "x:has(y):has(.unused)"',
start: {
- line: 50,
+ line: 52,
column: 1,
- character: 556
+ character: 578
},
end: {
- line: 50,
+ line: 52,
column: 22,
- character: 577
+ character: 599
}
},
{
code: 'css_unused_selector',
message: 'Unused CSS selector ".unused"',
start: {
- line: 69,
+ line: 71,
column: 2,
- character: 782
+ character: 804
},
end: {
- line: 69,
+ line: 71,
column: 9,
- character: 789
+ character: 811
}
},
{
code: 'css_unused_selector',
message: 'Unused CSS selector ".unused x:has(y)"',
start: {
- line: 85,
+ line: 87,
column: 1,
- character: 936
+ character: 958
},
end: {
- line: 85,
+ line: 87,
column: 17,
- character: 952
+ character: 974
}
},
{
code: 'css_unused_selector',
message: 'Unused CSS selector ".unused:has(.unused)"',
start: {
- line: 88,
+ line: 90,
column: 1,
- character: 973
+ character: 995
},
end: {
- line: 88,
+ line: 90,
column: 21,
- character: 993
+ character: 1015
}
},
{
code: 'css_unused_selector',
message: 'Unused CSS selector "x:has(> z)"',
start: {
- line: 98,
+ line: 100,
column: 1,
- character: 1093
+ character: 1115
},
end: {
- line: 98,
+ line: 100,
column: 11,
- character: 1103
+ character: 1125
}
},
{
code: 'css_unused_selector',
message: 'Unused CSS selector "x:has(> d)"',
start: {
- line: 101,
+ line: 103,
column: 1,
- character: 1124
+ character: 1146
},
end: {
- line: 101,
+ line: 103,
column: 11,
- character: 1134
+ character: 1156
}
},
{
code: 'css_unused_selector',
message: 'Unused CSS selector "x:has(~ y)"',
start: {
- line: 121,
+ line: 123,
column: 1,
- character: 1326
+ character: 1348
},
end: {
- line: 121,
+ line: 123,
column: 11,
- character: 1336
+ character: 1358
+ }
+ },
+ {
+ code: 'css_unused_selector',
+ message: 'Unused CSS selector "f:has(~ d)"',
+ start: {
+ line: 133,
+ column: 1,
+ character: 1446
+ },
+ end: {
+ line: 133,
+ column: 11,
+ character: 1456
}
},
{
code: 'css_unused_selector',
message: 'Unused CSS selector ":has(.unused)"',
start: {
- line: 129,
+ line: 141,
column: 2,
- character: 1409
+ character: 1529
},
end: {
- line: 129,
+ line: 141,
column: 15,
- character: 1422
+ character: 1542
}
},
{
code: 'css_unused_selector',
message: 'Unused CSS selector "&:has(.unused)"',
start: {
- line: 135,
+ line: 147,
column: 2,
- character: 1480
+ character: 1600
},
end: {
- line: 135,
+ line: 147,
column: 16,
- character: 1494
+ character: 1614
+ }
+ },
+ {
+ code: 'css_unused_selector',
+ message: 'Unused CSS selector ":global(.foo):has(.unused)"',
+ start: {
+ line: 155,
+ column: 1,
+ character: 1684
+ },
+ end: {
+ line: 155,
+ column: 27,
+ character: 1710
}
}
]
diff --git a/packages/svelte/tests/css/samples/has/expected.css b/packages/svelte/tests/css/samples/has/expected.css
index 68d6aad68a..b257370d61 100644
--- a/packages/svelte/tests/css/samples/has/expected.css
+++ b/packages/svelte/tests/css/samples/has/expected.css
@@ -112,6 +112,16 @@
color: red;
}*/
+ d.svelte-xyz:has(+ e:where(.svelte-xyz)) {
+ color: green;
+ }
+ d.svelte-xyz:has(~ f:where(.svelte-xyz)) {
+ color: green;
+ }
+ /* (unused) f:has(~ d) {
+ color: red;
+ }*/
+
.foo {
.svelte-xyz:has(x:where(.svelte-xyz)) {
color: green;
@@ -126,3 +136,10 @@
color: red;
}*/
}
+
+ .foo:has(x.svelte-xyz) {
+ color: green;
+ }
+ /* (unused) :global(.foo):has(.unused) {
+ color: red;
+ }*/
diff --git a/packages/svelte/tests/css/samples/has/input.svelte b/packages/svelte/tests/css/samples/has/input.svelte
index 3487b64e8c..9b254996bf 100644
--- a/packages/svelte/tests/css/samples/has/input.svelte
+++ b/packages/svelte/tests/css/samples/has/input.svelte
@@ -3,6 +3,8 @@
{#if foo}
+
+
{/if}
@@ -122,6 +124,16 @@
color: red;
}
+ d:has(+ e) {
+ color: green;
+ }
+ d:has(~ f) {
+ color: green;
+ }
+ f:has(~ d) {
+ color: red;
+ }
+
:global(.foo) {
:has(x) {
color: green;
@@ -136,4 +148,11 @@
color: red;
}
}
+
+ :global(.foo):has(x) {
+ color: green;
+ }
+ :global(.foo):has(.unused) {
+ color: red;
+ }
diff --git a/packages/svelte/tests/hydration/samples/repair-mismatched-a-href/_expected.html b/packages/svelte/tests/hydration/samples/repair-mismatched-a-href/_expected.html
index 2f5b652fac..e1076af2ec 100644
--- a/packages/svelte/tests/hydration/samples/repair-mismatched-a-href/_expected.html
+++ b/packages/svelte/tests/hydration/samples/repair-mismatched-a-href/_expected.html
@@ -1 +1 @@
-foo
+foo foo
diff --git a/packages/svelte/tests/hydration/samples/repair-mismatched-a-href/main.svelte b/packages/svelte/tests/hydration/samples/repair-mismatched-a-href/main.svelte
index be01d05f8e..3f0c988016 100644
--- a/packages/svelte/tests/hydration/samples/repair-mismatched-a-href/main.svelte
+++ b/packages/svelte/tests/hydration/samples/repair-mismatched-a-href/main.svelte
@@ -3,3 +3,4 @@
foo
+foo
diff --git a/packages/svelte/tests/runtime-runes/samples/attribute-boolean-case-insensitivity/_config.js b/packages/svelte/tests/runtime-runes/samples/attribute-boolean-case-insensitivity/_config.js
new file mode 100644
index 0000000000..d0b8a421b3
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/attribute-boolean-case-insensitivity/_config.js
@@ -0,0 +1,60 @@
+import { test } from '../../test';
+
+export default test({
+ // JSDOM lacks support for some of these attributes, so we'll skip it for now.
+ //
+ // See:
+ // - `async`: https://github.com/jsdom/jsdom/issues/1564
+ // - `nomodule`: https://github.com/jsdom/jsdom/issues/2475
+ // - `autofocus`: https://github.com/jsdom/jsdom/issues/3041
+ // - `inert`: https://github.com/jsdom/jsdom/issues/3605
+ // - etc...: https://github.com/jestjs/jest/issues/139#issuecomment-592673550
+ skip_mode: ['client'],
+
+ html: `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/attribute-boolean-case-insensitivity/main.svelte b/packages/svelte/tests/runtime-runes/samples/attribute-boolean-case-insensitivity/main.svelte
new file mode 100644
index 0000000000..e9e5a16168
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/attribute-boolean-case-insensitivity/main.svelte
@@ -0,0 +1,22 @@
+
+
+{#each attributeValues as val}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+{/each}
diff --git a/packages/svelte/tests/runtime-runes/samples/attribute-spread-input/_config.js b/packages/svelte/tests/runtime-runes/samples/attribute-spread-input/_config.js
new file mode 100644
index 0000000000..ab94125503
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/attribute-spread-input/_config.js
@@ -0,0 +1,33 @@
+import { flushSync } from 'svelte';
+import { test } from '../../test';
+
+export default test({
+ async test({ target, assert }) {
+ // Test for https://github.com/sveltejs/svelte/issues/15237
+ const [setValues, clearValue] = target.querySelectorAll('button');
+ const [text1, text2, check1, check2] = target.querySelectorAll('input');
+
+ assert.equal(text1.value, '');
+ assert.equal(text2.value, '');
+ assert.equal(check1.checked, false);
+ assert.equal(check2.checked, false);
+
+ flushSync(() => {
+ setValues.click();
+ });
+
+ assert.equal(text1.value, 'message');
+ assert.equal(text2.value, 'message');
+ assert.equal(check1.checked, true);
+ assert.equal(check2.checked, true);
+
+ flushSync(() => {
+ clearValue.click();
+ });
+
+ assert.equal(text1.value, '');
+ assert.equal(text2.value, '');
+ assert.equal(check1.checked, false);
+ assert.equal(check2.checked, false);
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/attribute-spread-input/main.svelte b/packages/svelte/tests/runtime-runes/samples/attribute-spread-input/main.svelte
new file mode 100644
index 0000000000..4bb4365ee2
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/attribute-spread-input/main.svelte
@@ -0,0 +1,22 @@
+
+
+setValues
+clearValues
+
+
+
+
+
+
diff --git a/packages/svelte/tests/runtime-runes/samples/custom-element-attributes/_config.js b/packages/svelte/tests/runtime-runes/samples/custom-element-attributes/_config.js
new file mode 100644
index 0000000000..7f406d8f0d
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/custom-element-attributes/_config.js
@@ -0,0 +1,24 @@
+import { test } from '../../test';
+
+export default test({
+ mode: ['client'],
+ async test({ assert, target }) {
+ const my_element = /** @type HTMLElement & { object: { test: true }; } */ (
+ target.querySelector('my-element')
+ );
+ assert.equal(my_element.getAttribute('string'), 'test');
+ assert.equal(my_element.hasAttribute('object'), false);
+ assert.deepEqual(my_element.object, { test: true });
+
+ const my_link = /** @type HTMLAnchorElement & { object: { test: true }; } */ (
+ target.querySelector('a')
+ );
+ assert.equal(my_link.getAttribute('string'), 'test');
+ assert.equal(my_link.hasAttribute('object'), false);
+ assert.deepEqual(my_link.object, { test: true });
+
+ const [value1, value2] = target.querySelectorAll('value-element');
+ assert.equal(value1.shadowRoot?.innerHTML, 'test ');
+ assert.equal(value2.shadowRoot?.innerHTML, 'test ');
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/custom-element-attributes/main.svelte b/packages/svelte/tests/runtime-runes/samples/custom-element-attributes/main.svelte
new file mode 100644
index 0000000000..4c98245e5b
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/custom-element-attributes/main.svelte
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
diff --git a/packages/svelte/tests/runtime-runes/samples/derived-unowned-11/Child.svelte b/packages/svelte/tests/runtime-runes/samples/derived-unowned-11/Child.svelte
new file mode 100644
index 0000000000..cd215304a3
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/derived-unowned-11/Child.svelte
@@ -0,0 +1,5 @@
+
+
+ value = 'a'}>change
diff --git a/packages/svelte/tests/runtime-runes/samples/derived-unowned-11/Child2.svelte b/packages/svelte/tests/runtime-runes/samples/derived-unowned-11/Child2.svelte
new file mode 100644
index 0000000000..a1d9f93bec
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/derived-unowned-11/Child2.svelte
@@ -0,0 +1,5 @@
+
+
+{disabled}
diff --git a/packages/svelte/tests/runtime-runes/samples/derived-unowned-11/_config.js b/packages/svelte/tests/runtime-runes/samples/derived-unowned-11/_config.js
new file mode 100644
index 0000000000..9948f91966
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/derived-unowned-11/_config.js
@@ -0,0 +1,16 @@
+import { flushSync } from 'svelte';
+import { test } from '../../test';
+
+export default test({
+ async test({ assert, target }) {
+ let [btn1, btn2] = target.querySelectorAll('button');
+
+ btn1?.click();
+ flushSync();
+
+ btn2?.click();
+ flushSync();
+
+ assert.htmlEqual(target.innerHTML, `change change \nfalse`);
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/derived-unowned-11/main.svelte b/packages/svelte/tests/runtime-runes/samples/derived-unowned-11/main.svelte
new file mode 100644
index 0000000000..0219acdf7f
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/derived-unowned-11/main.svelte
@@ -0,0 +1,11 @@
+
+
+
+
+
+
diff --git a/packages/svelte/tests/runtime-runes/samples/derived-unowned-12/_config.js b/packages/svelte/tests/runtime-runes/samples/derived-unowned-12/_config.js
new file mode 100644
index 0000000000..8cd4af0548
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/derived-unowned-12/_config.js
@@ -0,0 +1,25 @@
+import { flushSync } from 'svelte';
+import { test } from '../../test';
+
+export default test({
+ async test({ assert, target }) {
+ let [btn1, btn2] = target.querySelectorAll('button');
+
+ btn1?.click();
+ flushSync();
+
+ btn2?.click();
+ flushSync();
+
+ btn1?.click();
+ flushSync();
+
+ btn1?.click();
+ flushSync();
+
+ assert.htmlEqual(
+ target.innerHTML,
+ `linked.current \n3\ncount \n1`
+ );
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/derived-unowned-12/main.svelte b/packages/svelte/tests/runtime-runes/samples/derived-unowned-12/main.svelte
new file mode 100644
index 0000000000..48d4f5fd0b
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/derived-unowned-12/main.svelte
@@ -0,0 +1,18 @@
+
+
+ linked.current++}>linked.current {linked.current}
+ count++}>count {count}
diff --git a/packages/svelte/tests/runtime-runes/samples/effect-tracking-unowned/_config.js b/packages/svelte/tests/runtime-runes/samples/effect-tracking-unowned/_config.js
new file mode 100644
index 0000000000..749b9997c2
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/effect-tracking-unowned/_config.js
@@ -0,0 +1,16 @@
+import { flushSync } from 'svelte';
+import { test } from '../../test';
+
+export default test({
+ async test({ assert, target, logs }) {
+ const b1 = target.querySelector('button');
+
+ b1?.click();
+ flushSync();
+
+ assert.htmlEqual(
+ target.innerHTML,
+ `Store: new Text: new message
Change Store `
+ );
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/effect-tracking-unowned/main.svelte b/packages/svelte/tests/runtime-runes/samples/effect-tracking-unowned/main.svelte
new file mode 100644
index 0000000000..3c16e3c036
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/effect-tracking-unowned/main.svelte
@@ -0,0 +1,12 @@
+
+
+Store: {$store}
+Text: {text}
+ { store.set("new"); }}>Change Store
diff --git a/packages/svelte/tests/runtime-runes/samples/form-novalidate-casing/_config.js b/packages/svelte/tests/runtime-runes/samples/form-novalidate-casing/_config.js
new file mode 100644
index 0000000000..4fdf3632d6
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/form-novalidate-casing/_config.js
@@ -0,0 +1,8 @@
+import { test } from '../../test';
+
+export default test({
+ html: `
+
+
+`
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/form-novalidate-casing/main.svelte b/packages/svelte/tests/runtime-runes/samples/form-novalidate-casing/main.svelte
new file mode 100644
index 0000000000..1e8115ff62
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/form-novalidate-casing/main.svelte
@@ -0,0 +1,6 @@
+
+
+
+
diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-with-binding-8/CounterBinding.svelte b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-with-binding-8/CounterBinding.svelte
new file mode 100644
index 0000000000..d6da559fb1
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-with-binding-8/CounterBinding.svelte
@@ -0,0 +1,7 @@
+
+
+Binding
+ linked3.count++}>Increment Linked 1 ({linked3.count})
+ linked4.count++}>Increment Linked 2 ({linked4.count})
diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-with-binding-8/CounterContext.svelte b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-with-binding-8/CounterContext.svelte
new file mode 100644
index 0000000000..b935f0a472
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-with-binding-8/CounterContext.svelte
@@ -0,0 +1,13 @@
+
+
+Context
+ linked1.linked.current.count++}
+ >Increment Linked 1 ({linked1.linked.current.count})
+ linked2.linked.current.count++}
+ >Increment Linked 2 ({linked2.linked.current.count})
diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-with-binding-8/_config.js b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-with-binding-8/_config.js
new file mode 100644
index 0000000000..d6d12d01cd
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-with-binding-8/_config.js
@@ -0,0 +1,34 @@
+import { flushSync } from 'svelte';
+import { test } from '../../test';
+
+// Tests that ownership is widened with $derived (on class or on its own) that contains $state
+export default test({
+ compileOptions: {
+ dev: true
+ },
+
+ test({ assert, target, warnings }) {
+ const [root, counter_context1, counter_context2, counter_binding1, counter_binding2] =
+ target.querySelectorAll('button');
+
+ counter_context1.click();
+ counter_context2.click();
+ counter_binding1.click();
+ counter_binding2.click();
+ flushSync();
+
+ assert.equal(warnings.length, 0);
+
+ root.click();
+ flushSync();
+ counter_context1.click();
+ counter_context2.click();
+ counter_binding1.click();
+ counter_binding2.click();
+ flushSync();
+
+ assert.equal(warnings.length, 0);
+ },
+
+ warnings: []
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-with-binding-8/main.svelte b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-with-binding-8/main.svelte
new file mode 100644
index 0000000000..aaade26e16
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-with-binding-8/main.svelte
@@ -0,0 +1,46 @@
+
+
+Parent
+ counter.count++}>
+ Increment Original ({counter.count})
+
+
+
+
diff --git a/packages/svelte/tests/runtime-runes/samples/props-id/Child.svelte b/packages/svelte/tests/runtime-runes/samples/props-id/Child.svelte
new file mode 100644
index 0000000000..ad8bbd6f01
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/props-id/Child.svelte
@@ -0,0 +1,5 @@
+
+
+{id}
diff --git a/packages/svelte/tests/runtime-runes/samples/props-id/_config.js b/packages/svelte/tests/runtime-runes/samples/props-id/_config.js
new file mode 100644
index 0000000000..9d91b98e0f
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/props-id/_config.js
@@ -0,0 +1,61 @@
+import { flushSync } from 'svelte';
+import { test } from '../../test';
+
+export default test({
+ test({ assert, target, variant }) {
+ if (variant === 'dom') {
+ assert.htmlEqual(
+ target.innerHTML,
+ `
+ toggle
+ c1
+ c2
+ c3
+ c4
+ `
+ );
+ } else {
+ assert.htmlEqual(
+ target.innerHTML,
+ `
+ toggle
+ s1
+ s2
+ s3
+ s4
+ `
+ );
+ }
+
+ let button = target.querySelector('button');
+ flushSync(() => button?.click());
+
+ if (variant === 'dom') {
+ assert.htmlEqual(
+ target.innerHTML,
+ `
+ toggle
+ c1
+ c2
+ c3
+ c4
+ c5
+ `
+ );
+ } else {
+ // `c6` because this runs after the `dom` tests
+ // (slightly brittle but good enough for now)
+ assert.htmlEqual(
+ target.innerHTML,
+ `
+ toggle
+ s1
+ s2
+ s3
+ s4
+ c6
+ `
+ );
+ }
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/props-id/main.svelte b/packages/svelte/tests/runtime-runes/samples/props-id/main.svelte
new file mode 100644
index 0000000000..646bb2ebde
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/props-id/main.svelte
@@ -0,0 +1,19 @@
+
+
+ show = !show}>toggle
+
+{id}
+
+
+
+
+
+{#if show}
+
+{/if}
diff --git a/packages/svelte/tests/runtime-runes/samples/undefined-event-handler/_config.js b/packages/svelte/tests/runtime-runes/samples/undefined-event-handler/_config.js
new file mode 100644
index 0000000000..012fedb160
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/undefined-event-handler/_config.js
@@ -0,0 +1,9 @@
+import { ok, test } from '../../test';
+
+export default test({
+ async test({ target }) {
+ const button = target.querySelector('button');
+ ok(button);
+ button.dispatchEvent(new window.MouseEvent('mouseenter'));
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/undefined-event-handler/main.svelte b/packages/svelte/tests/runtime-runes/samples/undefined-event-handler/main.svelte
new file mode 100644
index 0000000000..ea4b4443e9
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/undefined-event-handler/main.svelte
@@ -0,0 +1 @@
+
diff --git a/packages/svelte/tests/runtime-runes/samples/untracked-derived-local/_config.js b/packages/svelte/tests/runtime-runes/samples/untracked-derived-local/_config.js
new file mode 100644
index 0000000000..1fe92ed2d7
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/untracked-derived-local/_config.js
@@ -0,0 +1,5 @@
+import { test } from '../../test';
+
+export default test({
+ html: `3`
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/untracked-derived-local/main.svelte b/packages/svelte/tests/runtime-runes/samples/untracked-derived-local/main.svelte
new file mode 100644
index 0000000000..0873eb741d
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/untracked-derived-local/main.svelte
@@ -0,0 +1,47 @@
+
+
+
+
+{test}
diff --git a/packages/svelte/tests/runtime-runes/samples/value-attribute-null-undefined/_config.js b/packages/svelte/tests/runtime-runes/samples/value-attribute-null-undefined/_config.js
new file mode 100644
index 0000000000..08f0c5aec7
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/value-attribute-null-undefined/_config.js
@@ -0,0 +1,49 @@
+import { test, ok } from '../../test';
+import { flushSync } from 'svelte';
+
+export default test({
+ mode: ['client'],
+
+ async test({ assert, target }) {
+ /**
+ * @type {HTMLInputElement | null}
+ */
+ const input = target.querySelector('input[type=text]');
+ /**
+ * @type {HTMLButtonElement | null}
+ */
+ const setString = target.querySelector('#setString');
+ /**
+ * @type {HTMLButtonElement | null}
+ */
+ const setNull = target.querySelector('#setNull');
+ /**
+ * @type {HTMLButtonElement | null}
+ */
+ const setUndefined = target.querySelector('#setUndefined');
+
+ ok(input);
+ ok(setString);
+ ok(setNull);
+ ok(setUndefined);
+
+ // value should always be blank string when value attribute is set to null or undefined
+
+ assert.equal(input.value, '');
+ setString.click();
+ flushSync();
+ assert.equal(input.value, 'foo');
+
+ setNull.click();
+ flushSync();
+ assert.equal(input.value, '');
+
+ setString.click();
+ flushSync();
+ assert.equal(input.value, 'foo');
+
+ setUndefined.click();
+ flushSync();
+ assert.equal(input.value, '');
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/value-attribute-null-undefined/main.svelte b/packages/svelte/tests/runtime-runes/samples/value-attribute-null-undefined/main.svelte
new file mode 100644
index 0000000000..9f944923c2
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/value-attribute-null-undefined/main.svelte
@@ -0,0 +1,9 @@
+
+
+
+
+ {value = "foo";}}>
+ {value = null;}}>
+ {value = undefined;}}>
diff --git a/packages/svelte/tests/signals/test.ts b/packages/svelte/tests/signals/test.ts
index e147fd1d0d..046f833e0e 100644
--- a/packages/svelte/tests/signals/test.ts
+++ b/packages/svelte/tests/signals/test.ts
@@ -1,18 +1,20 @@
import { describe, assert, it } from 'vitest';
import { flushSync } from '../../src/index-client';
import * as $ from '../../src/internal/client/runtime';
+import { push, pop } from '../../src/internal/client/context';
import {
effect,
effect_root,
render_effect,
user_effect
} from '../../src/internal/client/reactivity/effects';
-import { state, set } from '../../src/internal/client/reactivity/sources';
-import type { Derived, Value } from '../../src/internal/client/types';
+import { state, set, update, update_pre } from '../../src/internal/client/reactivity/sources';
+import type { Derived, Effect, Value } from '../../src/internal/client/types';
import { proxy } from '../../src/internal/client/proxy';
import { derived } from '../../src/internal/client/reactivity/deriveds';
import { snapshot } from '../../src/internal/shared/clone.js';
import { SvelteSet } from '../../src/reactivity/set';
+import { DESTROYED } from '../../src/internal/client/constants';
/**
* @param runes runes mode
@@ -22,13 +24,13 @@ import { SvelteSet } from '../../src/reactivity/set';
function run_test(runes: boolean, fn: (runes: boolean) => () => void) {
return () => {
// Create a component context to test runes vs legacy mode
- $.push({}, runes);
+ push({}, runes);
// Create a render context so that effect validations etc don't fail
let execute: any;
const destroy = effect_root(() => {
execute = fn(runes);
});
- $.pop();
+ pop();
execute();
destroy();
};
@@ -67,7 +69,7 @@ describe('signals', () => {
};
});
- test('multiple effects with state and derived in it#1', () => {
+ test('multiple effects with state and derived in it #1', () => {
const log: string[] = [];
let count = state(0);
@@ -88,7 +90,7 @@ describe('signals', () => {
};
});
- test('multiple effects with state and derived in it#2', () => {
+ test('multiple effects with state and derived in it #2', () => {
const log: string[] = [];
let count = state(0);
@@ -222,20 +224,75 @@ describe('signals', () => {
};
});
- test('effects correctly handle unowned derived values that do not change', () => {
- const log: number[] = [];
+ test('https://perf.js.hyoo.ru/#!bench=9h2as6_u0mfnn #2', () => {
+ let res: number[] = [];
- let count = state(0);
- const read = () => {
- const x = derived(() => ({ count: $.get(count) }));
- return $.get(x);
+ const numbers = Array.from({ length: 2 }, (_, i) => i);
+ const fib = (n: number): number => (n < 2 ? 1 : fib(n - 1) + fib(n - 2));
+ const hard = (n: number, l: string) => n + fib(16);
+
+ const A = state(0);
+ const B = state(0);
+
+ return () => {
+ const C = derived(() => ($.get(A) % 2) + ($.get(B) % 2));
+ const D = derived(() => numbers.map((i) => i + ($.get(A) % 2) - ($.get(B) % 2)));
+ const E = derived(() => hard($.get(C) + $.get(A) + $.get(D)[0]!, 'E'));
+ const F = derived(() => hard($.get(D)[0]! && $.get(B), 'F'));
+ const G = derived(() => $.get(C) + ($.get(C) || $.get(E) % 2) + $.get(D)[0]! + $.get(F));
+
+ const destroy = effect_root(() => {
+ effect(() => {
+ res.push(hard($.get(G), 'H'));
+ });
+ effect(() => {
+ res.push($.get(G));
+ });
+ effect(() => {
+ res.push(hard($.get(F), 'J'));
+ });
+ });
+
+ flushSync();
+
+ let i = 2;
+ while (--i) {
+ res.length = 0;
+ set(B, 1);
+ set(A, 1 + i * 2);
+ flushSync();
+
+ set(A, 2 + i * 2);
+ set(B, 2);
+ flushSync();
+
+ assert.equal(res.length, 4);
+ assert.deepEqual(res, [3198, 1601, 3195, 1598]);
+ }
+
+ destroy();
+ assert(A.reactions === null);
+ assert(B.reactions === null);
};
- const derivedCount = derived(() => read().count);
- user_effect(() => {
- log.push($.get(derivedCount));
- });
+ });
+
+ test('effects correctly handle unowned derived values that do not change', () => {
+ const log: number[] = [];
return () => {
+ let count = state(0);
+ const read = () => {
+ const x = derived(() => ({ count: $.get(count) }));
+ return $.get(x);
+ };
+ const derivedCount = derived(() => read().count);
+
+ const destroy = effect_root(() => {
+ user_effect(() => {
+ log.push($.get(derivedCount));
+ });
+ });
+
flushSync(() => set(count, 1));
// Ensure we're not leaking consumers
assert.deepEqual(count.reactions?.length, 1);
@@ -246,6 +303,8 @@ describe('signals', () => {
// Ensure we're not leaking consumers
assert.deepEqual(count.reactions?.length, 1);
assert.deepEqual(log, [0, 1, 2, 3]);
+
+ destroy();
};
});
@@ -255,12 +314,16 @@ describe('signals', () => {
const a = state(0);
const b = state(0);
- const c = derived(() => {
- const a_2 = derived(() => $.get(a) + '!');
- const b_2 = derived(() => $.get(b) + '?');
- nested.push(a_2, b_2);
+ let c: any;
+
+ const destroy = effect_root(() => {
+ c = derived(() => {
+ const a_2 = derived(() => $.get(a) + '!');
+ const b_2 = derived(() => $.get(b) + '?');
+ nested.push(a_2, b_2);
- return { a: $.get(a_2), b: $.get(b_2) };
+ return { a: $.get(a_2), b: $.get(b_2) };
+ });
});
$.get(c);
@@ -273,11 +336,10 @@ describe('signals', () => {
$.get(c);
- // Ensure we're not leaking dependencies
- assert.deepEqual(
- nested.slice(0, -2).map((s) => s.deps),
- [null, null, null, null]
- );
+ destroy();
+
+ assert.equal(a.reactions, null);
+ assert.equal(b.reactions, null);
};
});
@@ -338,25 +400,69 @@ describe('signals', () => {
};
});
- let some_state = state({});
- let some_deps = derived(() => {
- return [$.get(some_state)];
- });
-
test('two effects with an unowned derived that has some dependencies', () => {
const log: Array> = [];
- render_effect(() => {
- log.push($.get(some_deps));
- });
+ return () => {
+ let some_state = state({});
+ let some_deps = derived(() => {
+ return [$.get(some_state)];
+ });
+ let destroy2: any;
- render_effect(() => {
- log.push($.get(some_deps));
- });
+ const destroy = effect_root(() => {
+ render_effect(() => {
+ $.untrack(() => {
+ log.push($.get(some_deps));
+ });
+ });
- return () => {
+ destroy2 = effect_root(() => {
+ render_effect(() => {
+ log.push($.get(some_deps));
+ });
+
+ render_effect(() => {
+ log.push($.get(some_deps));
+ });
+ });
+ });
+
+ set(some_state, {});
flushSync();
- assert.deepEqual(log, [[{}], [{}]]);
+
+ assert.deepEqual(log, [[{}], [{}], [{}], [{}], [{}]]);
+
+ destroy2();
+
+ set(some_state, {});
+ flushSync();
+
+ assert.deepEqual(log, [[{}], [{}], [{}], [{}], [{}]]);
+
+ log.length = 0;
+
+ const destroy3 = effect_root(() => {
+ render_effect(() => {
+ $.untrack(() => {
+ log.push($.get(some_deps));
+ });
+ log.push($.get(some_deps));
+ });
+ });
+
+ set(some_state, {});
+ flushSync();
+
+ assert.deepEqual(log, [[{}], [{}], [{}], [{}]]);
+
+ destroy3();
+
+ assert(some_state.reactions === null);
+
+ destroy();
+
+ assert(some_state.reactions === null);
};
});
@@ -476,6 +582,7 @@ describe('signals', () => {
effect(() => {
log.push('inner', $.get(inner));
});
+ return $.get(outer);
});
});
});
@@ -529,6 +636,103 @@ describe('signals', () => {
};
});
+ test('mixed nested deriveds correctly cleanup when no longer connected to graph #1', () => {
+ let a: Derived;
+ let b: Derived;
+ let s = state(0);
+
+ const destroy = effect_root(() => {
+ render_effect(() => {
+ a = derived(() => {
+ b = derived(() => {
+ $.get(s);
+ });
+ $.untrack(() => {
+ $.get(b);
+ });
+ $.get(s);
+ });
+ $.get(a);
+ });
+ });
+
+ return () => {
+ flushSync();
+ assert.equal(a?.deps?.length, 1);
+ assert.equal(s?.reactions?.length, 1);
+ destroy();
+ assert.equal(s?.reactions, null);
+ };
+ });
+
+ test('mixed nested deriveds correctly cleanup when no longer connected to graph #2', () => {
+ let a: Derived;
+ let b: Derived;
+ let s = state(0);
+
+ const destroy = effect_root(() => {
+ render_effect(() => {
+ a = derived(() => {
+ b = derived(() => {
+ $.get(s);
+ });
+ effect_root(() => {
+ $.get(b);
+ });
+ $.get(s);
+ });
+ $.get(a);
+ });
+ });
+
+ return () => {
+ flushSync();
+ assert.equal(a?.deps?.length, 1);
+ assert.equal(s?.reactions?.length, 1);
+ destroy();
+ assert.equal(s?.reactions, null);
+ };
+ });
+
+ test('mixed nested deriveds correctly cleanup when no longer connected to graph #3', () => {
+ let a: Derived;
+ let b: Derived;
+ let s = state(0);
+ let logs: any[] = [];
+
+ const destroy = effect_root(() => {
+ render_effect(() => {
+ a = derived(() => {
+ b = derived(() => {
+ return $.get(s);
+ });
+ effect_root(() => {
+ $.get(b);
+ });
+ render_effect(() => {
+ logs.push($.get(b));
+ });
+ $.get(s);
+ });
+ $.get(a);
+ });
+ });
+
+ return () => {
+ flushSync();
+ assert.equal(a?.deps?.length, 1);
+ assert.equal(s?.reactions?.length, 2);
+
+ set(s, 1);
+ flushSync();
+
+ assert.deepEqual(logs, [0, 1]);
+
+ destroy();
+ assert.equal(s?.reactions, null);
+ };
+ });
+
test('deriveds update upon reconnection #1', () => {
let a = state(false);
let b = state(false);
@@ -715,6 +919,28 @@ describe('signals', () => {
};
});
+ test('unowned deriveds dependencies are correctly de-duped', () => {
+ return () => {
+ let a = state(0);
+ let b = state(true);
+ let c = derived(() => $.get(a));
+ let d = derived(() => ($.get(b) ? 1 : $.get(a) + $.get(c) + $.get(a)));
+
+ $.get(d);
+
+ assert.equal(d.deps?.length, 1);
+
+ $.get(d);
+
+ set(a, 1);
+ set(b, false);
+
+ $.get(d);
+
+ assert.equal(d.deps?.length, 3);
+ };
+ });
+
test('unowned deriveds correctly update', () => {
return () => {
const arr1 = proxy<{ a: number }[]>([]);
@@ -777,29 +1003,44 @@ describe('signals', () => {
};
});
- test('nested deriveds clean up the relationships when used with untrack', () => {
+ test('deriveds containing effects work correctly', () => {
return () => {
let a = render_effect(() => {});
+ let b = state(0);
+ let c;
+ let effects: Effect[] = [];
const destroy = effect_root(() => {
a = render_effect(() => {
- $.untrack(() => {
- const b = derived(() => {
- const c = derived(() => {});
- $.untrack(() => {
- $.get(c);
- });
- });
+ c = derived(() => {
+ effects.push(
+ effect(() => {
+ $.get(b);
+ })
+ );
$.get(b);
});
+ $.get(c);
});
});
- assert.deepEqual(a.deriveds?.length, 1);
+ assert.equal(c!.effects?.length, 1);
+ assert.equal(a.first, a.last);
+
+ set(b, 1);
+
+ flushSync();
+
+ assert.equal(c!.effects?.length, 1);
+ assert.equal(a.first, a.last);
destroy();
- assert.deepEqual(a.deriveds, null);
+ assert.equal(a.first, null);
+
+ assert.equal(effects.length, 2);
+ assert.equal((effects[0].f & DESTROYED) !== 0, true);
+ assert.equal((effects[1].f & DESTROYED) !== 0, true);
};
});
@@ -808,7 +1049,7 @@ describe('signals', () => {
let a = render_effect(() => {});
let b = state(0);
let c;
- let effects = [];
+ let effects: Effect[] = [];
const destroy = effect_root(() => {
a = render_effect(() => {
@@ -826,20 +1067,23 @@ describe('signals', () => {
});
});
- assert.deepEqual(c!.children?.length, 1);
- assert.deepEqual(a.first, a.last);
+ assert.equal(c!.effects?.length, 1);
+ assert.equal(a.first, a.last);
set(b, 1);
flushSync();
- assert.deepEqual(c!.children?.length, 1);
- assert.deepEqual(a.first, a.last);
+ assert.equal(c!.effects?.length, 1);
+ assert.equal(a.first, a.last);
destroy();
- assert.deepEqual(a.deriveds, null);
- assert.deepEqual(a.first, null);
+ assert.equal(a.first, null);
+
+ assert.equal(effects.length, 2);
+ assert.equal((effects[0].f & DESTROYED) !== 0, true);
+ assert.equal((effects[1].f & DESTROYED) !== 0, true);
};
});
@@ -847,14 +1091,14 @@ describe('signals', () => {
return () => {
const count = state(0n);
- assert.doesNotThrow(() => $.update(count));
+ assert.doesNotThrow(() => update(count));
assert.equal($.get(count), 1n);
- assert.doesNotThrow(() => $.update(count, -1));
+ assert.doesNotThrow(() => update(count, -1));
assert.equal($.get(count), 0n);
- assert.doesNotThrow(() => $.update_pre(count));
+ assert.doesNotThrow(() => update_pre(count));
assert.equal($.get(count), 1n);
- assert.doesNotThrow(() => $.update_pre(count, -1));
+ assert.doesNotThrow(() => update_pre(count, -1));
assert.equal($.get(count), 0n);
};
});
diff --git a/packages/svelte/tests/snapshot/samples/skip-static-subtree/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/skip-static-subtree/_expected/client/index.svelte.js
index 9b203b97e8..46d376aca2 100644
--- a/packages/svelte/tests/snapshot/samples/skip-static-subtree/_expected/client/index.svelte.js
+++ b/packages/svelte/tests/snapshot/samples/skip-static-subtree/_expected/client/index.svelte.js
@@ -42,12 +42,8 @@ export default function Skip_static_subtree($$anchor, $$props) {
$.reset(select);
var img = $.sibling(select, 2);
- var div_2 = $.sibling(img, 2);
- var img_1 = $.child(div_2);
- $.reset(div_2);
+ $.next(2);
$.template_effect(() => $.set_text(text, $$props.title));
- $.handle_lazy_img(img);
- $.handle_lazy_img(img_1);
$.append($$anchor, fragment);
}
\ No newline at end of file
diff --git a/packages/svelte/tests/types/bindings.svelte b/packages/svelte/tests/types/bindings.svelte
new file mode 100644
index 0000000000..ce99b2c296
--- /dev/null
+++ b/packages/svelte/tests/types/bindings.svelte
@@ -0,0 +1,8 @@
+
+
+
+
+
+
diff --git a/packages/svelte/tests/validator/samples/a11y-label-has-associated-control/input.svelte b/packages/svelte/tests/validator/samples/a11y-label-has-associated-control/input.svelte
index 124888c089..f47743b33b 100644
--- a/packages/svelte/tests/validator/samples/a11y-label-has-associated-control/input.svelte
+++ b/packages/svelte/tests/validator/samples/a11y-label-has-associated-control/input.svelte
@@ -10,3 +10,4 @@
E
F {#if true} {/if}
G
+E
\ No newline at end of file
diff --git a/packages/svelte/tests/validator/samples/a11y-mouse-events-have-key-events/input.svelte b/packages/svelte/tests/validator/samples/a11y-mouse-events-have-key-events/input.svelte
index 613b80e6d9..f9fe4f15c1 100644
--- a/packages/svelte/tests/validator/samples/a11y-mouse-events-have-key-events/input.svelte
+++ b/packages/svelte/tests/validator/samples/a11y-mouse-events-have-key-events/input.svelte
@@ -1,21 +1,19 @@
- void 0}>
+ {}}>
- void 0} on:focus={() => void 0}>
+ {}} onfocus={() => {}}>
- void 0} {...otherProps}>
+ {}} {...otherProps}>
- void 0}>
+ {}}>
- void 0} on:blur={() => void 0}>
+ {}} onblur={() => {}}>
- void 0} {...otherProps}>
+ {}} {...otherProps}>
diff --git a/packages/svelte/tests/validator/samples/a11y-mouse-events-have-key-events/warnings.json b/packages/svelte/tests/validator/samples/a11y-mouse-events-have-key-events/warnings.json
index 574b019e0f..3dee4e9673 100644
--- a/packages/svelte/tests/validator/samples/a11y-mouse-events-have-key-events/warnings.json
+++ b/packages/svelte/tests/validator/samples/a11y-mouse-events-have-key-events/warnings.json
@@ -2,49 +2,25 @@
{
"code": "a11y_mouse_events_have_key_events",
"end": {
- "column": 39,
- "line": 11
+ "column": 34,
+ "line": 9
},
"message": "'mouseover' event must be accompanied by 'focus' event",
"start": {
"column": 0,
- "line": 11
+ "line": 9
}
},
{
"code": "a11y_mouse_events_have_key_events",
"end": {
- "column": 55,
+ "column": 33,
"line": 15
},
- "message": "'mouseover' event must be accompanied by 'focus' event",
- "start": {
- "column": 0,
- "line": 15
- }
- },
- {
- "code": "a11y_mouse_events_have_key_events",
- "end": {
- "column": 38,
- "line": 17
- },
"message": "'mouseout' event must be accompanied by 'blur' event",
"start": {
"column": 0,
- "line": 17
- }
- },
- {
- "code": "a11y_mouse_events_have_key_events",
- "end": {
- "column": 54,
- "line": 21
- },
- "message": "'mouseout' event must be accompanied by 'blur' event",
- "start": {
- "column": 0,
- "line": 21
+ "line": 15
}
}
]
diff --git a/packages/svelte/tests/validator/samples/const-tag-invalid-rune-usage/errors.json b/packages/svelte/tests/validator/samples/const-tag-invalid-rune-usage/errors.json
new file mode 100644
index 0000000000..32594e4268
--- /dev/null
+++ b/packages/svelte/tests/validator/samples/const-tag-invalid-rune-usage/errors.json
@@ -0,0 +1,14 @@
+[
+ {
+ "code": "state_invalid_placement",
+ "message": "`$derived(...)` can only be used as a variable declaration initializer or a class field",
+ "start": {
+ "line": 2,
+ "column": 15
+ },
+ "end": {
+ "line": 2,
+ "column": 26
+ }
+ }
+]
diff --git a/packages/svelte/tests/validator/samples/const-tag-invalid-rune-usage/input.svelte b/packages/svelte/tests/validator/samples/const-tag-invalid-rune-usage/input.svelte
new file mode 100644
index 0000000000..a056058cc5
--- /dev/null
+++ b/packages/svelte/tests/validator/samples/const-tag-invalid-rune-usage/input.svelte
@@ -0,0 +1,3 @@
+{#snippet test()}
+ {@const der = $derived(0)}
+{/snippet}
\ No newline at end of file
diff --git a/packages/svelte/tests/validator/samples/mutate-derived-private-field/errors.json b/packages/svelte/tests/validator/samples/mutate-derived-private-field/errors.json
new file mode 100644
index 0000000000..fe51488c70
--- /dev/null
+++ b/packages/svelte/tests/validator/samples/mutate-derived-private-field/errors.json
@@ -0,0 +1 @@
+[]
diff --git a/packages/svelte/tests/validator/samples/mutate-derived-private-field/input.svelte b/packages/svelte/tests/validator/samples/mutate-derived-private-field/input.svelte
new file mode 100644
index 0000000000..0a3df4bc1b
--- /dev/null
+++ b/packages/svelte/tests/validator/samples/mutate-derived-private-field/input.svelte
@@ -0,0 +1,9 @@
+
\ No newline at end of file
diff --git a/packages/svelte/tests/validator/samples/reassign-derived-literal/errors.json b/packages/svelte/tests/validator/samples/reassign-derived-literal/errors.json
new file mode 100644
index 0000000000..8681d84ab2
--- /dev/null
+++ b/packages/svelte/tests/validator/samples/reassign-derived-literal/errors.json
@@ -0,0 +1,14 @@
+[
+ {
+ "code": "constant_assignment",
+ "message": "Cannot assign to derived state",
+ "start": {
+ "column": 3,
+ "line": 6
+ },
+ "end": {
+ "column": 29,
+ "line": 6
+ }
+ }
+]
diff --git a/packages/svelte/tests/validator/samples/reassign-derived-literal/input.svelte b/packages/svelte/tests/validator/samples/reassign-derived-literal/input.svelte
new file mode 100644
index 0000000000..8f109c9e1f
--- /dev/null
+++ b/packages/svelte/tests/validator/samples/reassign-derived-literal/input.svelte
@@ -0,0 +1,9 @@
+
\ No newline at end of file
diff --git a/packages/svelte/tests/validator/samples/reassign-derived-private-field/errors.json b/packages/svelte/tests/validator/samples/reassign-derived-private-field/errors.json
new file mode 100644
index 0000000000..c211aa4608
--- /dev/null
+++ b/packages/svelte/tests/validator/samples/reassign-derived-private-field/errors.json
@@ -0,0 +1,14 @@
+[
+ {
+ "code": "constant_assignment",
+ "message": "Cannot assign to derived state",
+ "start": {
+ "column": 3,
+ "line": 6
+ },
+ "end": {
+ "column": 27,
+ "line": 6
+ }
+ }
+]
diff --git a/packages/svelte/tests/validator/samples/reassign-derived-private-field/input.svelte b/packages/svelte/tests/validator/samples/reassign-derived-private-field/input.svelte
new file mode 100644
index 0000000000..62e2317e03
--- /dev/null
+++ b/packages/svelte/tests/validator/samples/reassign-derived-private-field/input.svelte
@@ -0,0 +1,9 @@
+
\ No newline at end of file
diff --git a/packages/svelte/tests/validator/samples/reassign-derived-public-field/errors.json b/packages/svelte/tests/validator/samples/reassign-derived-public-field/errors.json
new file mode 100644
index 0000000000..98837589ac
--- /dev/null
+++ b/packages/svelte/tests/validator/samples/reassign-derived-public-field/errors.json
@@ -0,0 +1,14 @@
+[
+ {
+ "code": "constant_assignment",
+ "message": "Cannot assign to derived state",
+ "start": {
+ "column": 3,
+ "line": 6
+ },
+ "end": {
+ "column": 26,
+ "line": 6
+ }
+ }
+]
diff --git a/packages/svelte/tests/validator/samples/reassign-derived-public-field/input.svelte b/packages/svelte/tests/validator/samples/reassign-derived-public-field/input.svelte
new file mode 100644
index 0000000000..e2c4693e86
--- /dev/null
+++ b/packages/svelte/tests/validator/samples/reassign-derived-public-field/input.svelte
@@ -0,0 +1,9 @@
+
\ No newline at end of file
diff --git a/packages/svelte/tests/validator/samples/slot-attribute-component/input.svelte b/packages/svelte/tests/validator/samples/slot-attribute-component/input.svelte
index 5acb14e409..5d559e614e 100644
--- a/packages/svelte/tests/validator/samples/slot-attribute-component/input.svelte
+++ b/packages/svelte/tests/validator/samples/slot-attribute-component/input.svelte
@@ -1,2 +1,8 @@
valid
valid
+
+ {#if true}
+ valid
+ valid
+ {/if}
+
\ No newline at end of file
diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts
index 176de125a6..b68e166de5 100644
--- a/packages/svelte/types/index.d.ts
+++ b/packages/svelte/types/index.d.ts
@@ -427,6 +427,34 @@ declare module 'svelte' {
}): Snippet;
/** Anything except a function */
type NotFunction = T extends Function ? never : T;
+ /**
+ * Retrieves the context that belongs to the closest parent component with the specified `key`.
+ * Must be called during component initialisation.
+ *
+ * */
+ export function getContext(key: any): T;
+ /**
+ * Associates an arbitrary `context` object with the current component and the specified `key`
+ * and returns that object. The context is then available to children of the component
+ * (including slotted content) with `getContext`.
+ *
+ * Like lifecycle functions, this must be called during component initialisation.
+ *
+ * */
+ export function setContext(key: any, context: T): T;
+ /**
+ * Checks whether a given `key` has been set in the context of a parent component.
+ * Must be called during component initialisation.
+ *
+ * */
+ export function hasContext(key: any): boolean;
+ /**
+ * Retrieves the whole context map that belongs to the closest parent component.
+ * Must be called during component initialisation. Useful, for example, if you
+ * programmatically create a component and want to pass the existing context to it.
+ *
+ * */
+ export function getAllContexts = Map>(): T;
/**
* Mounts a component to the given target and returns the exports and potentially the props (if compiled with `accessors: true`) of the component.
* Transitions will play during the initial render unless the `intro` option is set to `false`.
@@ -490,34 +518,6 @@ declare module 'svelte' {
* ```
* */
export function untrack(fn: () => T): T;
- /**
- * Retrieves the context that belongs to the closest parent component with the specified `key`.
- * Must be called during component initialisation.
- *
- * */
- export function getContext(key: any): T;
- /**
- * Associates an arbitrary `context` object with the current component and the specified `key`
- * and returns that object. The context is then available to children of the component
- * (including slotted content) with `getContext`.
- *
- * Like lifecycle functions, this must be called during component initialisation.
- *
- * */
- export function setContext(key: any, context: T): T;
- /**
- * Checks whether a given `key` has been set in the context of a parent component.
- * Must be called during component initialisation.
- *
- * */
- export function hasContext(key: any): boolean;
- /**
- * Retrieves the whole context map that belongs to the closest parent component.
- * Must be called during component initialisation. Useful, for example, if you
- * programmatically create a component and want to pass the existing context to it.
- *
- * */
- export function getAllContexts = Map>(): T;
export {};
}
@@ -3008,6 +3008,15 @@ declare namespace $effect {
declare function $props(): any;
declare namespace $props {
+ /**
+ * Generates an ID that is unique to the current component instance. When hydrating a server-rendered component,
+ * the value will be consistent between server and client.
+ *
+ * This is useful for linking elements via attributes like `for` and `aria-labelledby`.
+ * @since 5.20.0
+ */
+ export function id(): string;
+
// prevent intellisense from being unhelpful
/** @deprecated */
export const apply: never;
diff --git a/playgrounds/sandbox/index.html b/playgrounds/sandbox/index.html
index 512b5426a9..845538abf0 100644
--- a/playgrounds/sandbox/index.html
+++ b/playgrounds/sandbox/index.html
@@ -12,7 +12,7 @@