diff --git a/.changeset/hot-kings-shout.md b/.changeset/hot-kings-shout.md
deleted file mode 100644
index afba164abf..0000000000
--- a/.changeset/hot-kings-shout.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'svelte': patch
----
-
-fix: store access on component destroy
diff --git a/.changeset/spicy-insects-check.md b/.changeset/spicy-insects-check.md
deleted file mode 100644
index b998d36400..0000000000
--- a/.changeset/spicy-insects-check.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'svelte': patch
----
-
-fix: correctly transform `pre` with no content
diff --git a/.changeset/tender-apples-scream.md b/.changeset/tender-apples-scream.md
deleted file mode 100644
index 836bdaffdf..0000000000
--- a/.changeset/tender-apples-scream.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'svelte': patch
----
-
-fix: wrap each block expression in derived to encapsulate effects
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index b2bcb08848..cf73a1f6cb 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -12,6 +12,7 @@ env:
jobs:
Tests:
+ permissions: {}
runs-on: ${{ matrix.os }}
timeout-minutes: 15
strategy:
@@ -41,6 +42,7 @@ jobs:
env:
CI: true
Lint:
+ permissions: {}
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
@@ -61,6 +63,7 @@ jobs:
if: (${{ success() }} || ${{ failure() }}) # ensures this step runs even if previous steps fail
run: pnpm build && { [ "`git status --porcelain=v1`" == "" ] || (echo "Generated types have changed — please regenerate types locally with `cd packages/svelte && pnpm generate:types` and commit the changes after you have reviewed them"; git diff; exit 1); }
Benchmarks:
+ permissions: {}
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
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/ecosystem-ci-trigger.yml b/.github/workflows/ecosystem-ci-trigger.yml
index ce7bf04136..71df3242e8 100644
--- a/.github/workflows/ecosystem-ci-trigger.yml
+++ b/.github/workflows/ecosystem-ci-trigger.yml
@@ -9,6 +9,7 @@ jobs:
runs-on: ubuntu-latest
if: github.repository == 'sveltejs/svelte' && github.event.issue.pull_request && startsWith(github.event.comment.body, '/ecosystem-ci run')
steps:
+ - uses: GitHubSecurityLab/actions-permissions/monitor@v1
- uses: actions/github-script@v6
with:
script: |
diff --git a/.github/workflows/pkg.pr.new-comment.yml b/.github/workflows/pkg.pr.new-comment.yml
index 1698a456d3..3f1fca5a0b 100644
--- a/.github/workflows/pkg.pr.new-comment.yml
+++ b/.github/workflows/pkg.pr.new-comment.yml
@@ -6,11 +6,15 @@ on:
types:
- completed
+permissions:
+ pull-requests: write
+
jobs:
build:
name: 'Update comment'
runs-on: ubuntu-latest
steps:
+ - uses: GitHubSecurityLab/actions-permissions/monitor@v1
- name: Download artifact
uses: actions/download-artifact@v4
with:
diff --git a/.github/workflows/pkg.pr.new.yml b/.github/workflows/pkg.pr.new.yml
index 4292ec900a..b1ba217e5a 100644
--- a/.github/workflows/pkg.pr.new.yml
+++ b/.github/workflows/pkg.pr.new.yml
@@ -3,16 +3,16 @@ on: [push, pull_request]
jobs:
build:
+ permissions: {}
+
runs-on: ubuntu-latest
steps:
- - name: Checkout code
- uses: actions/checkout@v4
-
- - run: corepack enable
+ - uses: actions/checkout@v4
+ - uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
- node-version: 18.x
+ node-version: 22.x
cache: pnpm
- name: Install dependencies
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 1daef0b89c..6debe5662a 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -17,6 +17,7 @@ jobs:
name: Release
runs-on: ubuntu-latest
steps:
+ - uses: GitHubSecurityLab/actions-permissions/monitor@v1
- name: Checkout Repo
uses: actions/checkout@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 dd7bbb476e..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
@@ -62,8 +62,6 @@ When [opening a new issue](https://github.com/sveltejs/svelte/issues/new/choose)
## Pull requests
-> HEADS UP: Svelte 5 will likely change a lot on the compiler. For that reason, please don't open PRs that are large in scope, touch more than a couple of files etc. In other words, bug fixes are fine, but big feature PRs will likely not be merged.
-
### Proposing a change
If you would like to request a new feature or enhancement but are not yet thinking about opening a pull request, you can also file an issue with [feature template](https://github.com/sveltejs/svelte/issues/new?template=feature_request.yml).
diff --git a/LICENSE.md b/LICENSE.md
index abbace7bfe..f872adf738 100644
--- a/LICENSE.md
+++ b/LICENSE.md
@@ -1,4 +1,4 @@
-Copyright (c) 2016-2025 [these people](https://github.com/sveltejs/svelte/graphs/contributors)
+Copyright (c) 2016-2025 [Svelte Contributors](https://github.com/sveltejs/svelte/graphs/contributors)
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
diff --git a/README.md b/README.md
index cfb1328495..7ea7164752 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,11 @@
-[](https://svelte.dev)
-
-[](LICENSE.md) [](https://svelte.dev/chat)
+
+
+
+
+
+
+
+[](LICENSE.md) [](https://svelte.dev/chat)
## What is Svelte?
diff --git a/assets/banner.png b/assets/banner.png
new file mode 100644
index 0000000000..3428b278bf
Binary files /dev/null and b/assets/banner.png differ
diff --git a/assets/banner_dark.png b/assets/banner_dark.png
new file mode 100644
index 0000000000..1adba40d8e
Binary files /dev/null and b/assets/banner_dark.png differ
diff --git a/benchmarking/benchmarks/reactivity/kairo/kairo_avoidable.js b/benchmarking/benchmarks/reactivity/kairo/kairo_avoidable.js
index 6b058cdc3c..9daea6de99 100644
--- a/benchmarking/benchmarks/reactivity/kairo/kairo_avoidable.js
+++ b/benchmarking/benchmarks/reactivity/kairo/kairo_avoidable.js
@@ -20,12 +20,12 @@ function setup() {
return {
destroy,
run() {
- $.flush_sync(() => {
+ $.flush(() => {
$.set(head, 1);
});
assert($.get(computed5) === 6);
for (let i = 0; i < 1000; i++) {
- $.flush_sync(() => {
+ $.flush(() => {
$.set(head, i);
});
assert($.get(computed5) === 6);
diff --git a/benchmarking/benchmarks/reactivity/kairo/kairo_broad.js b/benchmarking/benchmarks/reactivity/kairo/kairo_broad.js
index d1cde5958e..8dc5710c87 100644
--- a/benchmarking/benchmarks/reactivity/kairo/kairo_broad.js
+++ b/benchmarking/benchmarks/reactivity/kairo/kairo_broad.js
@@ -25,12 +25,12 @@ function setup() {
return {
destroy,
run() {
- $.flush_sync(() => {
+ $.flush(() => {
$.set(head, 1);
});
counter = 0;
for (let i = 0; i < 50; i++) {
- $.flush_sync(() => {
+ $.flush(() => {
$.set(head, i);
});
assert($.get(last) === i + 50);
diff --git a/benchmarking/benchmarks/reactivity/kairo/kairo_deep.js b/benchmarking/benchmarks/reactivity/kairo/kairo_deep.js
index 149457ede1..8690c85f86 100644
--- a/benchmarking/benchmarks/reactivity/kairo/kairo_deep.js
+++ b/benchmarking/benchmarks/reactivity/kairo/kairo_deep.js
@@ -25,12 +25,12 @@ function setup() {
return {
destroy,
run() {
- $.flush_sync(() => {
+ $.flush(() => {
$.set(head, 1);
});
counter = 0;
for (let i = 0; i < iter; i++) {
- $.flush_sync(() => {
+ $.flush(() => {
$.set(head, i);
});
assert($.get(current) === len + i);
diff --git a/benchmarking/benchmarks/reactivity/kairo/kairo_diamond.js b/benchmarking/benchmarks/reactivity/kairo/kairo_diamond.js
index 958a1bcd78..bf4e07ee89 100644
--- a/benchmarking/benchmarks/reactivity/kairo/kairo_diamond.js
+++ b/benchmarking/benchmarks/reactivity/kairo/kairo_diamond.js
@@ -28,13 +28,13 @@ function setup() {
return {
destroy,
run() {
- $.flush_sync(() => {
+ $.flush(() => {
$.set(head, 1);
});
assert($.get(sum) === 2 * width);
counter = 0;
for (let i = 0; i < 500; i++) {
- $.flush_sync(() => {
+ $.flush(() => {
$.set(head, i);
});
assert($.get(sum) === (i + 1) * width);
diff --git a/benchmarking/benchmarks/reactivity/kairo/kairo_mux.js b/benchmarking/benchmarks/reactivity/kairo/kairo_mux.js
index b645051c09..fc252a27b5 100644
--- a/benchmarking/benchmarks/reactivity/kairo/kairo_mux.js
+++ b/benchmarking/benchmarks/reactivity/kairo/kairo_mux.js
@@ -22,13 +22,13 @@ function setup() {
destroy,
run() {
for (let i = 0; i < 10; i++) {
- $.flush_sync(() => {
+ $.flush(() => {
$.set(heads[i], i);
});
assert($.get(splited[i]) === i + 1);
}
for (let i = 0; i < 10; i++) {
- $.flush_sync(() => {
+ $.flush(() => {
$.set(heads[i], i * 2);
});
assert($.get(splited[i]) === i * 2 + 1);
diff --git a/benchmarking/benchmarks/reactivity/kairo/kairo_repeated.js b/benchmarking/benchmarks/reactivity/kairo/kairo_repeated.js
index 53b85acd37..3bee06ca0e 100644
--- a/benchmarking/benchmarks/reactivity/kairo/kairo_repeated.js
+++ b/benchmarking/benchmarks/reactivity/kairo/kairo_repeated.js
@@ -25,13 +25,13 @@ function setup() {
return {
destroy,
run() {
- $.flush_sync(() => {
+ $.flush(() => {
$.set(head, 1);
});
assert($.get(current) === size);
counter = 0;
for (let i = 0; i < 100; i++) {
- $.flush_sync(() => {
+ $.flush(() => {
$.set(head, i);
});
assert($.get(current) === i * size);
diff --git a/benchmarking/benchmarks/reactivity/kairo/kairo_triangle.js b/benchmarking/benchmarks/reactivity/kairo/kairo_triangle.js
index b9e2ad9fa4..11a419a52e 100644
--- a/benchmarking/benchmarks/reactivity/kairo/kairo_triangle.js
+++ b/benchmarking/benchmarks/reactivity/kairo/kairo_triangle.js
@@ -38,13 +38,13 @@ function setup() {
destroy,
run() {
const constant = count(width);
- $.flush_sync(() => {
+ $.flush(() => {
$.set(head, 1);
});
assert($.get(sum) === constant);
counter = 0;
for (let i = 0; i < 100; i++) {
- $.flush_sync(() => {
+ $.flush(() => {
$.set(head, i);
});
assert($.get(sum) === constant - width + i * width);
diff --git a/benchmarking/benchmarks/reactivity/kairo/kairo_unstable.js b/benchmarking/benchmarks/reactivity/kairo/kairo_unstable.js
index 0e783732dc..54eb732cb2 100644
--- a/benchmarking/benchmarks/reactivity/kairo/kairo_unstable.js
+++ b/benchmarking/benchmarks/reactivity/kairo/kairo_unstable.js
@@ -25,13 +25,13 @@ function setup() {
return {
destroy,
run() {
- $.flush_sync(() => {
+ $.flush(() => {
$.set(head, 1);
});
assert($.get(current) === 40);
counter = 0;
for (let i = 0; i < 100; i++) {
- $.flush_sync(() => {
+ $.flush(() => {
$.set(head, i);
});
}
diff --git a/benchmarking/benchmarks/reactivity/mol_bench.js b/benchmarking/benchmarks/reactivity/mol_bench.js
index c9f492f619..536b078d74 100644
--- a/benchmarking/benchmarks/reactivity/mol_bench.js
+++ b/benchmarking/benchmarks/reactivity/mol_bench.js
@@ -51,11 +51,11 @@ function setup() {
*/
run(i) {
res.length = 0;
- $.flush_sync(() => {
+ $.flush(() => {
$.set(B, 1);
$.set(A, 1 + i * 2);
});
- $.flush_sync(() => {
+ $.flush(() => {
$.set(A, 2 + i * 2);
$.set(B, 2);
});
diff --git a/benchmarking/compare/index.js b/benchmarking/compare/index.js
index a5fc6d10a9..9d8d279c35 100644
--- a/benchmarking/compare/index.js
+++ b/benchmarking/compare/index.js
@@ -2,7 +2,6 @@ import fs from 'node:fs';
import path from 'node:path';
import { execSync, fork } from 'node:child_process';
import { fileURLToPath } from 'node:url';
-import { benchmarks } from '../benchmarks.js';
// if (execSync('git status --porcelain').toString().trim()) {
// console.error('Working directory is not clean');
diff --git a/benchmarking/compare/runner.js b/benchmarking/compare/runner.js
index 6fa58e2bac..a2e8646379 100644
--- a/benchmarking/compare/runner.js
+++ b/benchmarking/compare/runner.js
@@ -1,7 +1,7 @@
-import { benchmarks } from '../benchmarks.js';
+import { reactivity_benchmarks } from '../benchmarks/reactivity/index.js';
const results = [];
-for (const benchmark of benchmarks) {
+for (const benchmark of reactivity_benchmarks) {
const result = await benchmark();
console.error(result.benchmark);
results.push(result);
diff --git a/documentation/docs/01-introduction/02-getting-started.md b/documentation/docs/01-introduction/02-getting-started.md
index e035e6d6df..c7351729ff 100644
--- a/documentation/docs/01-introduction/02-getting-started.md
+++ b/documentation/docs/01-introduction/02-getting-started.md
@@ -2,7 +2,7 @@
title: Getting started
---
-We recommend using [SvelteKit](../kit), the official application framework from the Svelte team powered by [Vite](https://vite.dev/):
+We recommend using [SvelteKit](../kit), which lets you [build almost anything](../kit/project-types). It's the official application framework from the Svelte team and powered by [Vite](https://vite.dev/). Create a new project with:
```bash
npx sv create myapp
@@ -15,7 +15,9 @@ Don't worry if you don't know Svelte yet! You can ignore all the nice features S
## Alternatives to SvelteKit
-You can also use Svelte directly with Vite by running `npm create vite@latest` and selecting the `svelte` option. With this, `npm run build` will generate HTML, JS and CSS files inside the `dist` directory using [vite-plugin-svelte](https://github.com/sveltejs/vite-plugin-svelte). In most cases, you will probably need to [choose a routing library](faq#Is-there-a-router) as well.
+You can also use Svelte directly with Vite by running `npm create vite@latest` and selecting the `svelte` option. With this, `npm run build` will generate HTML, JS, and CSS files inside the `dist` directory using [vite-plugin-svelte](https://github.com/sveltejs/vite-plugin-svelte). In most cases, you will probably need to [choose a routing library](faq#Is-there-a-router) as well.
+
+>[!NOTE] Vite is often used in standalone mode to build [single page apps (SPAs)](../kit/glossary#SPA), which you can also [build with SvelteKit](../kit/single-page-apps).
There are also plugins for [Rollup](https://github.com/sveltejs/rollup-plugin-svelte), [Webpack](https://github.com/sveltejs/svelte-loader) [and a few others](https://sveltesociety.dev/packages?category=build-plugins), but we recommend Vite.
diff --git a/documentation/docs/01-introduction/04-svelte-js-files.md b/documentation/docs/01-introduction/04-svelte-js-files.md
index 0e05484299..1d3e3dd61a 100644
--- a/documentation/docs/01-introduction/04-svelte-js-files.md
+++ b/documentation/docs/01-introduction/04-svelte-js-files.md
@@ -4,7 +4,7 @@ title: .svelte.js and .svelte.ts files
Besides `.svelte` files, Svelte also operates on `.svelte.js` and `.svelte.ts` files.
-These behave like any other `.js` or `.ts` module, except that you can use runes. This is useful for creating reusable reactive logic, or sharing reactive state across your app.
+These behave like any other `.js` or `.ts` module, except that you can use runes. This is useful for creating reusable reactive logic, or sharing reactive state across your app (though note that you [cannot export reassigned state]($state#Passing-state-across-modules)).
> [!LEGACY]
> This is a concept that didn't exist prior to Svelte 5
diff --git a/documentation/docs/01-introduction/xx-props.md b/documentation/docs/01-introduction/xx-props.md
deleted file mode 100644
index cad854d878..0000000000
--- a/documentation/docs/01-introduction/xx-props.md
+++ /dev/null
@@ -1,139 +0,0 @@
----
-title: Public API of a component
----
-
-### Public API of a component
-
-Svelte uses the `$props` rune to declare _properties_ or _props_, which means describing the public interface of the component which becomes accessible to consumers of the component.
-
-> [!NOTE] `$props` is one of several runes, which are special hints for Svelte's compiler to make things reactive.
-
-```svelte
-
-```
-
-You can specify a fallback value for a prop. It will be used if the component's consumer doesn't specify the prop on the component when instantiating the component, or if the passed value is `undefined` at some point.
-
-```svelte
-
-```
-
-To get all properties, use rest syntax:
-
-```svelte
-
-```
-
-You can use reserved words as prop names.
-
-```svelte
-
-```
-
-If you're using TypeScript, you can declare the prop types:
-
-```svelte
-
-```
-
-If you're using JavaScript, you can declare the prop types using JSDoc:
-
-```svelte
-
-```
-
-If you export a `const`, `class` or `function`, it is readonly from outside the component.
-
-```svelte
-
-```
-
-Readonly props can be accessed as properties on the element, tied to the component using [`bind:this` syntax](bindings#bind:this).
-
-### Reactive variables
-
-To change component state and trigger a re-render, just assign to a locally declared variable that was declared using the `$state` rune.
-
-Update expressions (`count += 1`) and property assignments (`obj.x = y`) have the same effect.
-
-```svelte
-
-```
-
-Svelte's `
-```
-
-If you'd like to react to changes to a prop, use the `$derived` or `$effect` runes instead.
-
-```svelte
-
-```
-
-For more information on reactivity, read the documentation around runes.
diff --git a/documentation/docs/01-introduction/xx-reactivity-fundamentals.md b/documentation/docs/01-introduction/xx-reactivity-fundamentals.md
deleted file mode 100644
index d5e67ada71..0000000000
--- a/documentation/docs/01-introduction/xx-reactivity-fundamentals.md
+++ /dev/null
@@ -1,144 +0,0 @@
----
-title: Reactivity fundamentals
----
-
-Reactivity is at the heart of interactive UIs. When you click a button, you expect some kind of response. It's your job as a developer to make this happen. It's Svelte's job to make your job as intuitive as possible, by providing a good API to express reactive systems.
-
-## Runes
-
-Svelte 5 uses _runes_, a powerful set of primitives for controlling reactivity inside your Svelte components and inside `.svelte.js` and `.svelte.ts` modules.
-
-Runes are function-like symbols that provide instructions to the Svelte compiler. You don't need to import them from anywhere — when you use Svelte, they're part of the language.
-
-The following sections introduce the most important runes for declare state, derived state and side effects at a high level. For more details refer to the later sections on [state](state) and [side effects](side-effects).
-
-## `$state`
-
-Reactive state is declared with the `$state` rune:
-
-```svelte
-
-
- count++}>
- clicks: {count}
-
-```
-
-You can also use `$state` in class fields (whether public or private):
-
-```js
-// @errors: 7006 2554
-class Todo {
- done = $state(false);
- text = $state();
-
- constructor(text) {
- this.text = text;
- }
-}
-```
-
-> [!LEGACY]
-> In Svelte 4, state was implicitly reactive if the variable was declared at the top level
->
-> ```svelte
->
->
-> count++}>
-> clicks: {count}
->
-> ```
-
-## `$derived`
-
-Derived state is declared with the `$derived` rune:
-
-```svelte
-
-
- count++}>
- {doubled}
-
-
-
{count} doubled is {doubled}
-```
-
-The expression inside `$derived(...)` should be free of side-effects. Svelte will disallow state changes (e.g. `count++`) inside derived expressions.
-
-As with `$state`, you can mark class fields as `$derived`.
-
-> [!LEGACY]
-> In Svelte 4, you could use reactive statements for this.
->
-> ```svelte
->
->
-> count++}>
-> {doubled}
->
->
-> {count} doubled is {doubled}
-> ```
->
-> This only worked at the top level of a component.
-
-## `$effect`
-
-To run _side-effects_ when the component is mounted to the DOM, and when values change, we can use the `$effect` rune ([demo](/playground/untitled#H4sIAAAAAAAAE31T24rbMBD9lUG7kAQ2sbdlX7xOYNk_aB_rQhRpbAsU2UiTW0P-vbrYubSlYGzmzMzROTPymdVKo2PFjzMzfIusYB99z14YnfoQuD1qQh-7bmdFQEonrOppVZmKNBI49QthCc-OOOH0LZ-9jxnR6c7eUpOnuv6KeT5JFdcqbvbcBcgDz1jXKGg6ncFyBedYR6IzLrAZwiN5vtSxaJA-EzadfJEjKw11C6GR22-BLH8B_wxdByWpvUYtqqal2XB6RVkG1CoHB6U1WJzbnYFDiwb3aGEdDa3Bm1oH12sQLTcNPp7r56m_00mHocSG97_zd7ICUXonA5fwKbPbkE2ZtMJGGVkEdctzQi4QzSwr9prnFYNk5hpmqVuqPQjNnfOJoMF22lUsrq_UfIN6lfSVyvQ7grB3X2mjMZYO3XO9w-U5iLx42qg29md3BP_ni5P4gy9ikTBlHxjLzAtPDlyYZmRdjAbGq7HprEQ7p64v4LU_guu0kvAkhBim3nMplWl8FreQD-CW20aZR0wq12t-KqDWeBywhvexKC3memmDwlHAv9q4Vo2ZK8KtK0CgX7u9J8wXbzdKv-nRnfF_2baTqlYoWUF2h5efl9-n0O6koAMAAA==)):
-
-```svelte
-
-
-
-```
-
-The function passed to `$effect` will run when the component mounts, and will re-run after any changes to the values it reads that were declared with `$state` or `$derived` (including those passed in with `$props`). Re-runs are batched (i.e. changing `color` and `size` in the same moment won't cause two separate runs), and happen after any DOM updates have been applied.
-
-> [!LEGACY]
-> In Svelte 4, you could use reactive statements for this.
->
-> ```svelte
->
->
->
-> ```
->
-> This only worked at the top level of a component.
diff --git a/documentation/docs/02-runes/01-what-are-runes.md b/documentation/docs/02-runes/01-what-are-runes.md
index dc163ebdf1..59c371eb49 100644
--- a/documentation/docs/02-runes/01-what-are-runes.md
+++ b/documentation/docs/02-runes/01-what-are-runes.md
@@ -2,7 +2,7 @@
title: What are runes?
---
-> [!NOTE] **rune** /ro͞on/ _noun_
+> [!NOTE] **rune** /ruːn/ _noun_
>
> A letter or mark used as a mystical or magic symbol.
diff --git a/documentation/docs/02-runes/02-$state.md b/documentation/docs/02-runes/02-$state.md
index 49e17cd08f..16630a977b 100644
--- a/documentation/docs/02-runes/02-$state.md
+++ b/documentation/docs/02-runes/02-$state.md
@@ -250,3 +250,83 @@ console.log(total.value); // 7
```
...though if you find yourself writing code like that, consider using [classes](#Classes) instead.
+
+## Passing state across modules
+
+You can declare state in `.svelte.js` and `.svelte.ts` files, but you can only _export_ that state if it's not directly reassigned. In other words you can't do this:
+
+```js
+/// file: state.svelte.js
+export let count = $state(0);
+
+export function increment() {
+ count += 1;
+}
+```
+
+That's because every reference to `count` is transformed by the Svelte compiler — the code above is roughly equivalent to this:
+
+```js
+/// file: state.svelte.js (compiler output)
+// @filename: index.ts
+interface Signal {
+ value: T;
+}
+
+interface Svelte {
+ state(value?: T): Signal;
+ get(source: Signal): T;
+ set(source: Signal, value: T): void;
+}
+declare const $: Svelte;
+// ---cut---
+export let count = $.state(0);
+
+export function increment() {
+ $.set(count, $.get(count) + 1);
+}
+```
+
+> [!NOTE] You can see the code Svelte generates by clicking the 'JS Output' tab in the [playground](/playground).
+
+Since the compiler only operates on one file at a time, if another file imports `count` Svelte doesn't know that it needs to wrap each reference in `$.get` and `$.set`:
+
+```js
+// @filename: state.svelte.js
+export let count = 0;
+
+// @filename: index.js
+// ---cut---
+import { count } from './state.svelte.js';
+
+console.log(typeof count); // 'object', not 'number'
+```
+
+This leaves you with two options for sharing state between modules — either don't reassign it...
+
+```js
+// This is allowed — since we're updating
+// `counter.count` rather than `counter`,
+// Svelte doesn't wrap it in `$.state`
+export const counter = $state({
+ count: 0
+});
+
+export function increment() {
+ counter.count += 1;
+}
+```
+
+...or don't directly export it:
+
+```js
+let count = $state(0);
+
+export function getCount() {
+ return count;
+}
+
+export function increment() {
+ count += 1;
+}
+```
diff --git a/documentation/docs/02-runes/03-$derived.md b/documentation/docs/02-runes/03-$derived.md
index 24ab643b68..2464aa9295 100644
--- a/documentation/docs/02-runes/03-$derived.md
+++ b/documentation/docs/02-runes/03-$derived.md
@@ -52,6 +52,48 @@ Anything read synchronously inside the `$derived` expression (or `$derived.by` f
To exempt a piece of state from being treated as a dependency, use [`untrack`](svelte#untrack).
+## Overriding derived values
+
+Derived expressions are recalculated when their dependencies change, but you can temporarily override their values by reassigning them (unless they are declared with `const`). This can be useful for things like _optimistic UI_, where a value is derived from the 'source of truth' (such as data from your server) but you'd like to show immediate feedback to the user:
+
+```svelte
+
+
+🧡 {likes}
+```
+
+> [!NOTE] Prior to Svelte 5.25, deriveds were read-only.
+
+## Deriveds and reactivity
+
+Unlike `$state`, which converts objects and arrays to [deeply reactive proxies]($state#Deep-state), `$derived` values are left as-is. For example, [in a case like this](/playground/untitled#H4sIAAAAAAAAE4VU22rjMBD9lUHd3aaQi9PdstS1A3t5XvpQ2Ic4D7I1iUUV2UjjNMX431eS7TRdSosxgjMzZ45mjt0yzffIYibvy0ojFJWqDKCQVBk2ZVup0LJ43TJ6rn2aBxw-FP2o67k9oCKP5dziW3hRaUJNjoYltjCyplWmM1JIIAn3FlL4ZIkTTtYez6jtj4w8WwyXv9GiIXiQxLVs9pfTMR7EuoSLIuLFbX7Z4930bZo_nBrD1bs834tlfvsBz9_SyX6PZXu9XaL4gOWn4sXjeyzftv4ZWfyxubpzxzg6LfD4MrooxELEosKCUPigQCMPKCZh0OtQE1iSxcsmdHuBvCiHZXALLXiN08EL3RRkaJ_kDVGle0HcSD5TPEeVtj67O4Nrg9aiSNtBY5oODJkrL5QsHtN2cgXp6nSJMWzpWWGasdlsGEMbzi5jPr5KFr0Ep7pdeM2-TCelCddIhDxAobi1jqF3cMaC1RKp64bAW9iFAmXGIHfd4wNXDabtOLN53w8W53VvJoZLh7xk4Rr3CoL-UNoLhWHrT1JQGcM17u96oES5K-kc2XOzkzqGCKL5De79OUTyyrg1zgwXsrEx3ESfx4Bz0M5UjVMHB24mw9SuXtXFoN13fYKOM1tyUT3FbvbWmSWCZX2Er-41u5xPoml45svRahl9Wb9aasbINJixDZwcPTbyTLZSUsAvrg_cPuCR7s782_WU8343Y72Qtlb8OYatwuOQvuN13M_hJKNfxann1v1U_B1KZ_D_mzhzhz24fw85CSz2irtN9w9HshBK7AQAAA==)...
+
+```svelte
+let items = $state([...]);
+
+let index = $state(0);
+let selected = $derived(items[index]);
+```
+
+...you can change (or `bind:` to) properties of `selected` and it will affect the underlying `items` array. If `items` was _not_ deeply reactive, mutating `selected` would have no effect.
+
## Update propagation
Svelte uses something called _push-pull reactivity_ — when state is updated, everything that depends on the state (whether directly or indirectly) is immediately notified of the change (the 'push'), but derived values are not re-evaluated until they are actually read (the 'pull').
diff --git a/documentation/docs/02-runes/04-$effect.md b/documentation/docs/02-runes/04-$effect.md
index 1ea960de70..46ea9b81e9 100644
--- a/documentation/docs/02-runes/04-$effect.md
+++ b/documentation/docs/02-runes/04-$effect.md
@@ -2,15 +2,11 @@
title: $effect
---
-Effects are what make your application _do things_. When Svelte runs an effect function, it tracks which pieces of state (and derived state) are accessed (unless accessed inside [`untrack`](svelte#untrack)), and re-runs the function when that state later changes.
+Effects are functions that run when state updates, and can be used for things like calling third-party libraries, drawing on `` elements, or making network requests. They only run in the browser, not during server-side rendering.
-Most of the effects in a Svelte app are created by Svelte itself — they're the bits that update the text in `hello {name}! ` when `name` changes, for example.
+Generally speaking, you should _not_ update state inside effects, as it will make code more convoluted and will often lead to never-ending update cycles. If you find yourself doing so, see [when not to use `$effect`](#When-not-to-use-$effect) to learn about alternative approaches.
-But you can also create your own effects with the `$effect` rune, which is useful when you need to synchronize an external system (whether that's a library, or a `` element, or something across a network) with state inside your Svelte app.
-
-> [!NOTE] Avoid overusing `$effect`! When you do too much work in effects, code often becomes difficult to understand and maintain. See [when not to use `$effect`](#When-not-to-use-$effect) to learn about alternative approaches.
-
-Your effects run after the component has been mounted to the DOM, and in a [microtask](https://developer.mozilla.org/en-US/docs/Web/API/HTML_DOM_API/Microtask_guide) after state changes ([demo](/playground/untitled#H4sIAAAAAAAAE31S246bMBD9lZF3pSRSAqTVvrCAVPUP2sdSKY4ZwJJjkD0hSVH-vbINuWxXfQH5zMyZc2ZmZLVUaFn6a2R06ZGlHmBrpvnBvb71fWQHVOSwPbf4GS46TajJspRlVhjZU1HqkhQSWPkHIYdXS5xw-Zas3ueI6FRn7qHFS11_xSRZhIxbFtcDtw7SJb1iXaOg5XIFeQGjzyPRaevYNOGZIJ8qogbpe8CWiy_VzEpTXiQUcvPDkSVrSNZz1UlW1N5eLcqmpdXUvaQ4BmqlhZNUCgxuzFHDqUWNAxrYeUM76AzsnOsdiJbrBp_71lKpn3RRbii-4P3f-IMsRxS-wcDV_bL4PmSdBa2wl7pKnbp8DMgVvJm8ZNskKRkEM_OzyOKQFkgqOYBQ3Nq89Ns0nbIl81vMFN-jKoLMTOr-SOBOJS-Z8f5Y6D1wdcR8dFqvEBdetK-PHwj-z-cH8oHPY54wRJ8Ys7iSQ3Bg3VA9azQbmC9k35kKzYa6PoVtfwbbKVnBixBiGn7Pq0rqJoUtHiCZwAM3jdTPWCVtr_glhVrhecIa3vuksJ_b7TqFs4DPyriSjd5IwoNNQaAmNI-ESfR2p8zimzvN1swdCkvJHPH6-_oX8o1SgcIDAAA=)):
+You can create an effect with the `$effect` rune ([demo](/playground/untitled#H4sIAAAAAAAAE31S246bMBD9lZF3pSRSAqTVvrCAVPUP2sdSKY4ZwJJjkD0hSVH-vbINuWxXfQH5zMyZc2ZmZLVUaFn6a2R06ZGlHmBrpvnBvb71fWQHVOSwPbf4GS46TajJspRlVhjZU1HqkhQSWPkHIYdXS5xw-Zas3ueI6FRn7qHFS11_xSRZhIxbFtcDtw7SJb1iXaOg5XIFeQGjzyPRaevYNOGZIJ8qogbpe8CWiy_VzEpTXiQUcvPDkSVrSNZz1UlW1N5eLcqmpdXUvaQ4BmqlhZNUCgxuzFHDqUWNAxrYeUM76AzsnOsdiJbrBp_71lKpn3RRbii-4P3f-IMsRxS-wcDV_bL4PmSdBa2wl7pKnbp8DMgVvJm8ZNskKRkEM_OzyOKQFkgqOYBQ3Nq89Ns0nbIl81vMFN-jKoLMTOr-SOBOJS-Z8f5Y6D1wdcR8dFqvEBdetK-PHwj-z-cH8oHPY54wRJ8Ys7iSQ3Bg3VA9azQbmC9k35kKzYa6PoVtfwbbKVnBixBiGn7Pq0rqJoUtHiCZwAM3jdTPWCVtr_glhVrhecIa3vuksJ_b7TqFs4DPyriSjd5IwoNNQaAmNI-ESfR2p8zimzvN1swdCkvJHPH6-_oX8o1SgcIDAAA=)):
```svelte
-
+
```
-Re-runs are batched (i.e. changing `color` and `size` in the same moment won't cause two separate runs), and happen after any DOM updates have been applied.
+When Svelte runs an effect function, it tracks which pieces of state (and derived state) are accessed (unless accessed inside [`untrack`](svelte#untrack)), and re-runs the function when that state later changes.
+
+> [!NOTE] If you're having difficulty understanding why your `$effect` is rerunning or is not running see [understanding dependencies](#Understanding-dependencies). Effects are triggered differently than the `$:` blocks you may be used to if coming from Svelte 4.
+
+### Understanding lifecycle
+
+Your effects run after the component has been mounted to the DOM, and in a [microtask](https://developer.mozilla.org/en-US/docs/Web/API/HTML_DOM_API/Microtask_guide) after state changes. Re-runs are batched (i.e. changing `color` and `size` in the same moment won't cause two separate runs), and happen after any DOM updates have been applied.
+
+You can use `$effect` anywhere, not just at the top level of a component, as long as it is called while a parent effect is running.
-You can place `$effect` anywhere, not just at the top level of a component, as long as it is called during component initialization (or while a parent effect is active). It is then tied to the lifecycle of the component (or parent effect) and will therefore destroy itself when the component unmounts (or the parent effect is destroyed).
+> [!NOTE] Svelte uses effects internally to represent logic and expressions in your template — this is how `hello {name}! ` updates when `name` changes.
-You can return a function from `$effect`, which will run immediately before the effect re-runs, and before it is destroyed ([demo](/playground/untitled#H4sIAAAAAAAAE42RQY-bMBCF_8rI2kPopiXpMQtIPfbeW6m0xjyKtWaM7CFphPjvFVB2k2oPe7LmzXzyezOjaqxDVKefo5JrD3VaBLVXrLu5-tb3X-IZTmat0hHv6cazgCWqk8qiCbaXouRSHISMH1gop4coWrA7JE9bp7PO2QjjuY5vA8fDYZ3hUh7QNDCy2yWUFzTOUilpSj9aG-linaMKFGACtKCmSwvGGYGeLQvCWbtnMq3m34grajxHoa1JOUXI93_V_Sfz7Oz7Mafj0ypN-zvHm8dSAmQITP_xaUq2IU1GO1dp80I2Uh_82dao92Rl9R8GvgF0QrbrUFstcFeq0PgAkha0LoICPoeB4w1SJUvsZcj4rvcMlvmvGlGCv6J-DeSgw2vabQnJlm55p7nM0rcTctYei3HZxZSl7XHVqkHEM3k2zpqXfFyj393zU05fpyI6f0HI0hUoPoamC9roKDeo2ivBH1EnCQOmX9NfYw2GHrgCAAA=)).
+An effect can return a _teardown function_ which will run immediately before the effect re-runs ([demo](/playground/untitled#H4sIAAAAAAAAE42SQVODMBCF_8pOxkPRKq3HCsx49K4n64xpskjGkDDJ0tph-O8uINo6HjxB3u7HvrehE07WKDbiyZEhi1osRWksRrF57gQdm6E2CKx_dd43zU3co6VB28mIf-nKO0JH_BmRRRVMQ8XWbXkAgfKtI8jhIpIkXKySu7lSG2tNRGZ1_GlYr1ZTD3ddYFmiosUigbyAbpC2lKbwWJkIB8ZhhxBQBWRSw6FCh3sM8GrYTthL-wqqku4N44TyqEgwF3lmRHr4Op0PGXoH31c5rO8mqV-eOZ49bikgtcHBL55tmhIkEMqg_cFB2TpFxjtg703we6NRL8HQFCS07oSUCZi6Rm04lz1yytIHBKoQpo1w6Gsm4gmyS8b8Y5PydeMdX8gwS2Ok4I-ov5NZtvQde95GMsccn_1wzNKfu3RZtS66cSl9lvL7qO1aIk7knbJGvefdtIOzi73M4bYvovUHDFk6AcX_0HRESxnpBOW_jfCDxIZCi_1L_wm4xGQ60wIAAA==)).
```svelte
+// later...
+destroy();
```
## When not to use `$effect`
@@ -246,6 +267,8 @@ In general, `$effect` is best considered something of an escape hatch — useful
> [!NOTE] For things that are more complicated than a simple expression like `count * 2`, you can also use `$derived.by`.
+If you're using an effect because you want to be able to reassign the derived value (to build an optimistic UI, for example) note that [deriveds can be directly overridden]($derived#Overriding-derived-values) as of Svelte 5.25.
+
You might be tempted to do something convoluted with effects to link one value to another. The following example shows two inputs for "money spent" and "money left" that are connected to each other. If you update one, the other should update accordingly. Don't use effects for this ([demo](/playground/untitled#H4sIAAAAAAAACpVRy26DMBD8FcvKgUhtoIdeHBwp31F6MGSJkBbHwksEQvx77aWQqooq9bgzOzP7mGTdIHipPiZJowOpGJAv0po2VmfnDv4OSBErjYdneHWzBJaCjcx91TWOToUtCIEE3cig0OIty44r5l1oDtjOkyFIsv3GINQ_CNYyGegd1DVUlCR7oU9iilDUcP8S8roYs9n8p2wdYNVFm4csTx872BxNCcjr5I11fdgonEkXsjP2CoUUZWMv6m6wBz2x7yxaM-iJvWeRsvSbSVeUy5i0uf8vKA78NIeJLSZWv1I8jQjLdyK4XuTSeIdmVKJGGI4LdjVOiezwDu1yG74My8PLCQaSiroe5s_5C2PHrkVGAgAA)):
```svelte
@@ -274,7 +297,7 @@ You might be tempted to do something convoluted with effects to link one value t
```
-Instead, use callbacks where possible ([demo](/playground/untitled#H4sIAAAAAAAACo1SMW6EMBD8imWluFMSIEUaDiKlvy5lSOHjlhOSMRZeTiDkv8deMEEJRcqdmZ1ZjzzxqpZgePo5cRw18JQA_sSVaPz0rnVk7iDRYxdhYA8vW4Wg0NnwzJRdrfGtUAVKQIYtCsly9pIkp4AZ7cQOezAoEA7JcWUkVBuCdol0dNWrEutWsV5fHfnhPQ5wZJMnCwyejxCh6G6A0V3IHk4zu_jOxzzPBxBld83PTr7xXrb3rUNw8PbiYJ3FP22oTIoLSComq5XuXTeu8LzgnVA3KDgj13wiQ8taRaJ82rzXskYM-URRlsXktejjgNLoo9e4fyf70_8EnwncySX1GuunX6kGRwnzR_BgaPNaGy3FmLJKwrCUeBM6ZUn0Cs2mOlp3vwthQJ5i14P9st9vZqQlsQIAAA==)):
+Instead, use `oninput` callbacks or — better still — [function bindings](bind#Function-bindings) where possible ([demo](/playground/untitled#H4sIAAAAAAAAE51SsW6DMBT8FcvqABINdOhCIFKXTt06lg4GHpElYyz8iECIf69tcIIipo6-u3f3fPZMJWuBpvRzkBXyTpKSy5rLq6YRbbgATdOfmeKkrMgCBt9GPpQ66RsItFjJNBzhVScRJBobmumq5wovhSxQABLskAmSk7ckOXtMKyM22ItGhhAk4Z0R0OwIN-tIQzd-90HVhvy2HsGNiQFCMltBgd7XoecV2xzXNV7XaEcth7ZfRv7kujnsTX2Qd7USb5rFjwZkJlgJwpWRcakG04cpOS9oz-QVCuoeInXW-RyEJL-sG0b7Wy6kZWM-u7CFxM5tdrIl9qg72vB74H-y7T2iXROHyVb0CLanp1yNk4D1A1jQ91hzrQSbUtIIGLcir0ylJDm9Q7urz42bX4UwIk2xH2D5Xf4A7SeMcMQCAAA=)):
```svelte
-
+ spent, updateSpent} max={total} />
{spent}/{total} spent
-
+ left, updateLeft} max={total} />
{left}/{total} left
```
-If you need to use bindings, for whatever reason (for example when you want some kind of "writable `$derived`"), consider using getters and setters to synchronise state ([demo](/playground/untitled#H4sIAAAAAAAACpWRwW6DMBBEf8WyekikFOihFwcq9TvqHkyyQUjGsfCCQMj_XnvBNKpy6Qn2DTOD1wu_tRocF18Lx9kCFwT4iRvVxenT2syNoDGyWjl4xi93g2AwxPDSXfrW4oc0EjUgwzsqzSr2VhTnxJwNHwf24lAhHIpjVDZNwy1KS5wlNoGMSg9wOCYksQccerMlv65p51X0p_Xpdt_4YEy9yTkmV3z4MJT579-bUqsaNB2kbI0dwlnCgirJe2UakJzVrbkKaqkWivasU1O1ULxnOVk3JU-Uxti0p_-vKO4no_enbQ_yXhnZn0aHs4b1jiJMK7q2zmo1C3bTMG3LaZQVrMjeoSPgaUtkDxePMCEX2Ie6b_8D4WyJJEwCAAA=)):
-
-```svelte
-
-
-
-
- {spent}/{total} spent
-
-
-
-
- {left.value}/{total} left
-
-```
-
If you absolutely have to update `$state` within an effect and run into an infinite loop because you read and write to the same `$state`, use [untrack](svelte#untrack).
diff --git a/documentation/docs/02-runes/05-$props.md b/documentation/docs/02-runes/05-$props.md
index 4b1775bf5a..222b4831b6 100644
--- a/documentation/docs/02-runes/05-$props.md
+++ b/documentation/docs/02-runes/05-$props.md
@@ -37,7 +37,7 @@ On the other side, inside `MyComponent.svelte`, we can receive props with the `$
## Fallback values
-Destructuring allows us to declare fallback values, which are used if the parent component does not set a given prop:
+Destructuring allows us to declare fallback values, which are used if the parent component does not set a given prop (or the value is `undefined`):
```js
let { adjective = 'happy' } = $props();
@@ -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
+
+
+
+```
diff --git a/documentation/docs/02-runes/06-$bindable.md b/documentation/docs/02-runes/06-$bindable.md
index 14bc8ddbec..c12c2bf490 100644
--- a/documentation/docs/02-runes/06-$bindable.md
+++ b/documentation/docs/02-runes/06-$bindable.md
@@ -33,7 +33,7 @@ Now, a component that uses `` can add the [`bind:`](bind) directive
```svelte
-/// App.svelte
+/// file: App.svelte
-
+
```
Components also support `bind:this`, allowing you to interact with component instances programmatically.
diff --git a/documentation/docs/03-template-syntax/13-transition.md b/documentation/docs/03-template-syntax/13-transition.md
index 51c11e8b34..c51175c272 100644
--- a/documentation/docs/03-template-syntax/13-transition.md
+++ b/documentation/docs/03-template-syntax/13-transition.md
@@ -22,10 +22,6 @@ The `transition:` directive indicates a _bidirectional_ transition, which means
{/if}
```
-## Built-in transitions
-
-A selection of built-in transitions can be imported from the [`svelte/transition`](svelte-transition) module.
-
## Local vs global
Transitions are local by default. Local transitions only play when the block they belong to is created or destroyed, _not_ when parent blocks are created or destroyed.
@@ -40,6 +36,10 @@ Transitions are local by default. Local transitions only play when the block the
{/if}
```
+## Built-in transitions
+
+A selection of built-in transitions can be imported from the [`svelte/transition`](svelte-transition) module.
+
## Transition parameters
Transitions can have parameters.
diff --git a/documentation/docs/03-template-syntax/18-class.md b/documentation/docs/03-template-syntax/18-class.md
index 880a34e9ec..1ea4a208df 100644
--- a/documentation/docs/03-template-syntax/18-class.md
+++ b/documentation/docs/03-template-syntax/18-class.md
@@ -71,6 +71,18 @@ The user of this component has the same flexibility to use a mixture of objects,
```
+Svelte also exposes the `ClassValue` type, which is the type of value that the `class` attribute on elements accept. This is useful if you want to use a type-safe class name in component props:
+
+```svelte
+
+
+...
+```
+
## The `class:` directive
Prior to Svelte 5.16, the `class:` directive was the most convenient way to set classes on elements conditionally.
diff --git a/documentation/docs/03-template-syntax/xx-control-flow.md b/documentation/docs/03-template-syntax/xx-control-flow.md
deleted file mode 100644
index b73917997b..0000000000
--- a/documentation/docs/03-template-syntax/xx-control-flow.md
+++ /dev/null
@@ -1,111 +0,0 @@
----
-title: Control flow
----
-
-- if
-- each
-- await (or move that into some kind of data loading section?)
-- NOT: key (move into transition section, because that's the common use case)
-
-Svelte augments HTML with control flow blocks to be able to express conditionally rendered content or lists.
-
-The syntax between these blocks is the same:
-
-- `{#` denotes the start of a block
-- `{:` denotes a different branch part of the block. Depending on the block, there can be multiple of these
-- `{/` denotes the end of a block
-
-## {#if ...}
-
-## {#each ...}
-
-```svelte
-
-{#each expression as name}...{/each}
-```
-
-```svelte
-
-{#each expression as name, index}...{/each}
-```
-
-```svelte
-
-{#each expression as name (key)}...{/each}
-```
-
-```svelte
-
-{#each expression as name, index (key)}...{/each}
-```
-
-```svelte
-
-{#each expression as name}...{:else}...{/each}
-```
-
-Iterating over lists of values can be done with an each block.
-
-```svelte
-Shopping list
-
- {#each items as item}
- {item.name} x {item.qty}
- {/each}
-
-```
-
-You can use each blocks to iterate over any array or array-like value — that is, any object with a `length` property.
-
-An each block can also specify an _index_, equivalent to the second argument in an `array.map(...)` callback:
-
-```svelte
-{#each items as item, i}
- {i + 1}: {item.name} x {item.qty}
-{/each}
-```
-
-If a _key_ expression is provided — which must uniquely identify each list item — Svelte will use it to diff the list when data changes, rather than adding or removing items at the end. The key can be any object, but strings and numbers are recommended since they allow identity to persist when the objects themselves change.
-
-```svelte
-{#each items as item (item.id)}
- {item.name} x {item.qty}
-{/each}
-
-
-{#each items as item, i (item.id)}
- {i + 1}: {item.name} x {item.qty}
-{/each}
-```
-
-You can freely use destructuring and rest patterns in each blocks.
-
-```svelte
-{#each items as { id, name, qty }, i (id)}
- {i + 1}: {name} x {qty}
-{/each}
-
-{#each objects as { id, ...rest }}
- {id}
-{/each}
-
-{#each items as [id, ...rest]}
- {id}
-{/each}
-```
-
-An each block can also have an `{:else}` clause, which is rendered if the list is empty.
-
-```svelte
-{#each todos as todo}
- {todo.text}
-{:else}
- No tasks today!
-{/each}
-```
-
-It is possible to iterate over iterables like `Map` or `Set`. Iterables need to be finite and static (they shouldn't change while being iterated over). Under the hood, they are transformed to an array using `Array.from` before being passed off to rendering. If you're writing performance-sensitive code, try to avoid iterables and use regular arrays as they are more performant.
-
-## Other block types
-
-Svelte also provides [`#snippet`](snippets), [`#key`](transitions-and-animations) and [`#await`](data-fetching) blocks. You can find out more about them in their respective sections.
diff --git a/documentation/docs/03-template-syntax/xx-data-fetching.md b/documentation/docs/03-template-syntax/xx-data-fetching.md
deleted file mode 100644
index 4526d51335..0000000000
--- a/documentation/docs/03-template-syntax/xx-data-fetching.md
+++ /dev/null
@@ -1,20 +0,0 @@
----
-title: Data fetching
----
-
-Fetching data is a fundamental part of apps interacting with the outside world. Svelte is unopinionated with how you fetch your data. The simplest way would be using the built-in `fetch` method:
-
-```svelte
-
-```
-
-While this works, it makes working with promises somewhat unergonomic. Svelte alleviates this problem using the `#await` block.
-
-## {#await ...}
-
-## SvelteKit loaders
-
-Fetching inside your components is great for simple use cases, but it's prone to data loading waterfalls and makes code harder to work with because of the promise handling. SvelteKit solves this problem by providing a opinionated data loading story that is coupled to its router. Learn more about it [in the docs](../kit).
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/02-context.md b/documentation/docs/06-runtime/02-context.md
index 30799215b6..4204bcfe6d 100644
--- a/documentation/docs/06-runtime/02-context.md
+++ b/documentation/docs/06-runtime/02-context.md
@@ -2,129 +2,137 @@
title: Context
---
-
+Context allows components to access values owned by parent components without passing them down as props (potentially through many layers of intermediate components, known as 'prop-drilling'). The parent component sets context with `setContext(key, value)`...
-Most state is component-level state that lives as long as its component lives. There's also section-wide or app-wide state however, which also needs to be handled somehow.
-
-The easiest way to do that is to create global state and just import that.
+```svelte
+
+
```
+...and the child retrieves it with `getContext`:
+
```svelte
-
+
+
+{message}, inside Child.svelte
```
-This has a few drawbacks though:
+This is particularly useful when `Parent.svelte` is not directly aware of `Child.svelte`, but instead renders it as part of a `children` [snippet](snippet) ([demo](/playground/untitled#H4sIAAAAAAAAE42Q3W6DMAyFX8WyJgESK-oto6hTX2D3YxcM3IIUQpR40yqUd58CrCXsp7tL7HNsf2dAWXaEKR56yfTBGOOxFWQwfR6Qz8q1XAHjL-GjUhvzToJd7bU09FO9ctMkG0wxM5VuFeeFLLjtVK8ZnkpNkuGo-w6CTTJ9Z3PwsBAemlbUF934W8iy5DpaZtOUcU02-ZLcaS51jHEkTFm_kY1_wfOO8QnXrb8hBzDEc6pgZ4gFoyz4KgiD7nxfTe8ghqAhIfrJ46cTzVZBbkPlODVJsLCDO6V7ZcJoncyw1yRr0hd1GNn_ZbEM3I9i1bmVxOlWElUvDUNHxpQngt3C4CXzjS1rtvkw22wMrTRtTbC8Lkuabe7jvthPPe3DofYCAAA=)):
+
+```svelte
+
+
+
+```
-- it only safely works when your global state is only used client-side - for example, when you're building a single page application that does not render any of your components on the server. If your state ends up being managed and updated on the server, it could end up being shared between sessions and/or users, causing bugs
-- it may give the false impression that certain state is global when in reality it should only used in a certain part of your app
+The key (`'my-context'`, in the example above) and the context itself can be any JavaScript value.
-To solve these drawbacks, Svelte provides a few `context` primitives which alleviate these problems.
+In addition to [`setContext`](svelte#setContext) and [`getContext`](svelte#getContext), Svelte exposes [`hasContext`](svelte#hasContext) and [`getAllContexts`](svelte#getAllContexts) functions.
-## Setting and getting context
+## Using context with state
-To associate an arbitrary object with the current component, use `setContext`.
+You can store reactive state in context ([demo](/playground/untitled#H4sIAAAAAAAAE41R0W6DMAz8FSuaBNUQdK8MkKZ-wh7HHihzu6hgosRMm1D-fUpSVNq12x4iEvvOx_kmQU2PIhfP3DCCJGgHYvxkkYid7NCI_GUS_KUcxhVEMjOelErNB3bsatvG4LW6n0ZsRC4K02qpuKqpZtmrQTNMYJA3QRAs7PTQQxS40eMCt3mX3duxnWb-lS5h7nTI0A4jMWoo4c44P_Hku-zrOazdy64chWo-ScfRkRgl8wgHKrLTH1OxHZkHgoHaTraHcopXUFYzPPVfuC_hwQaD1GrskdiNCdQwJljJqlvXfyqVsA5CGg0uRUQifHw56xFtciO75QrP07vo_JXf_tf8yK2ezDKY_ZWt_1y2qqYzv7bI1IW1V_sN19m-07wCAAA=))...
```svelte
+
+ counter.count += 1}>
+ increment
+
+
+
+
+
```
-The context is then available to children of the component (including slotted content) with `getContext`.
+...though note that if you _reassign_ `counter` instead of updating it, you will 'break the link' — in other words instead of this...
```svelte
-
+ counter = { count: 0 }}>
+ reset
+
```
-`setContext` and `getContext` solve the above problems:
+...you must do this:
-- the state is not global, it's scoped to the component. That way it's safe to render your components on the server and not leak state
-- it's clear that the state is not global but rather scoped to a specific component tree and therefore can't be used in other parts of your app
+```svelte
+ +++counter.count = 0+++}>
+ reset
+
+```
-> [!NOTE] `setContext`/`getContext` must be called during component initialisation.
+Svelte will warn you if you get it wrong.
-Context is not inherently reactive. If you need reactive values in context then you can pass a `$state` object into context, whose properties _will_ be reactive.
+## Type-safe context
-```svelte
-
-
+```js
+/// file: context.js
+// @filename: ambient.d.ts
+interface User {}
- value.count++}>increment
-```
+// @filename: index.js
+// ---cut---
+import { getContext, setContext } from 'svelte';
-```svelte
-
-
+/** @param {User} user */
+export function setUserContext(user) {
+ setContext(key, user);
+}
-Count is {value.count}
+export function getUserContext() {
+ return /** @type {User} */ (getContext(key));
+}
```
-To check whether a given `key` has been set in the context of a parent component, use `hasContext`.
+## Replacing global state
-```svelte
-
+ // ...
+});
```
-You can also retrieve the whole context map that belongs to the closest parent component using `getAllContexts`. This is useful, for example, if you programmatically create a component and want to pass the existing context to it.
+In many cases this is perfectly fine, but there is a risk: if you mutate the state during server-side rendering (which is discouraged, but entirely possible!)...
```svelte
+
```
-## Encapsulating context interactions
-
-The above methods are very unopinionated about how to use them. When your app grows in scale, it's worthwhile to encapsulate setting and getting the context into functions and properly type them.
-
-```ts
-// @errors: 2304
-import { getContext, setContext } from 'svelte';
-
-let userKey = Symbol('user');
-
-export function setUserContext(user: User) {
- setContext(userKey, user);
-}
-
-export function getUserContext(): User {
- return getContext(userKey) as User;
-}
-```
+...then the data may be accessible by the _next_ user. Context solves this problem because it is not shared between requests.
diff --git a/documentation/docs/06-runtime/03-lifecycle-hooks.md b/documentation/docs/06-runtime/03-lifecycle-hooks.md
index a3dbe04b00..f051c46d73 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.
@@ -149,7 +147,7 @@ With runes, we can use `$effect.pre`, which behaves the same as `$effect` but ru
}
function toggle() {
- toggleValue = !toggleValue;
+ theme = theme === 'dark' ? 'light' : 'dark';
}
diff --git a/documentation/docs/07-misc/07-v5-migration-guide.md b/documentation/docs/07-misc/07-v5-migration-guide.md
index ce95bf6ac7..e502b7921a 100644
--- a/documentation/docs/07-misc/07-v5-migration-guide.md
+++ b/documentation/docs/07-misc/07-v5-migration-guide.md
@@ -10,13 +10,13 @@ You don't have to migrate to the new syntax right away - Svelte 5 still supports
At the heart of Svelte 5 is the new runes API. Runes are basically compiler instructions that inform Svelte about reactivity. Syntactically, runes are functions starting with a dollar-sign.
-### let -> $state
+### let → $state
In Svelte 4, a `let` declaration at the top level of a component was implicitly reactive. In Svelte 5, things are more explicit: a variable is reactive when created using the `$state` rune. Let's migrate the counter to runes mode by wrapping the counter in `$state`:
```svelte
```
@@ -25,14 +25,14 @@ Nothing else changes. `count` is still the number itself, and you read and write
> [!DETAILS] Why we did this
> `let` being implicitly reactive at the top level worked great, but it meant that reactivity was constrained - a `let` declaration anywhere else was not reactive. This forced you to resort to using stores when refactoring code out of the top level of components for reuse. This meant you had to learn an entirely separate reactivity model, and the result often wasn't as nice to work with. Because reactivity is more explicit in Svelte 5, you can keep using the same API outside the top level of components. Head to [the tutorial](/tutorial) to learn more.
-### $: -> $derived/$effect
+### $: → $derived/$effect
In Svelte 4, a `$:` statement at the top level of a component could be used to declare a derivation, i.e. state that is entirely defined through a computation of other state. In Svelte 5, this is achieved using the `$derived` rune:
```svelte
```
@@ -42,7 +42,8 @@ A `$:` statement could also be used to create side effects. In Svelte 5, this is
```svelte
```
+Note that [when `$effect` runs is different]($effect#Understanding-dependencies) than when `$:` runs.
+
> [!DETAILS] Why we did this
> `$:` was a great shorthand and easy to get started with: you could slap a `$:` in front of most code and it would somehow work. This intuitiveness was also its drawback the more complicated your code became, because it wasn't as easy to reason about. Was the intent of the code to create a derivation, or a side effect? With `$derived` and `$effect`, you have a bit more up-front decision making to do (spoiler alert: 90% of the time you want `$derived`), but future-you and other developers on your team will have an easier time.
>
@@ -71,14 +74,14 @@ A `$:` statement could also be used to create side effects. In Svelte 5, this is
> - executing dependencies as needed and therefore being immune to ordering problems
> - being TypeScript-friendly
-### export let -> $props
+### export let → $props
In Svelte 4, properties of a component were declared using `export let`. Each property was one declaration. In Svelte 5, all properties are declared through the `$props` rune, through destructuring:
```svelte
```
@@ -103,8 +106,8 @@ In Svelte 5, the `$props` rune makes this straightforward without any additional
```svelte
click me
@@ -190,9 +193,9 @@ This function is deprecated in Svelte 5. Instead, components should accept _call
```svelte
@@ -464,11 +467,11 @@ By now you should have a pretty good understanding of the before/after and how t
We thought the same, which is why we provide a migration script to do most of the migration automatically. You can upgrade your project by using `npx sv migrate svelte-5`. This will do the following things:
- bump core dependencies in your `package.json`
-- migrate to runes (`let` -> `$state` etc)
-- migrate to event attributes for DOM elements (`on:click` -> `onclick`)
-- migrate slot creations to render tags (` ` -> `{@render children()}`)
-- migrate slot usages to snippets (`...
` -> `{#snippet x()}...
{/snippet}`)
-- migrate obvious component creations (`new Component(...)` -> `mount(Component, ...)`)
+- migrate to runes (`let` → `$state` etc)
+- migrate to event attributes for DOM elements (`on:click` → `onclick`)
+- migrate slot creations to render tags (` ` → `{@render children()}`)
+- migrate slot usages to snippets (`...
` → `{#snippet x()}...
{/snippet}`)
+- migrate obvious component creations (`new Component(...)` → `mount(Component, ...)`)
You can also migrate a single component in VS Code through the `Migrate Component to Svelte 5 Syntax` command, or in our Playground through the `Migrate` button.
@@ -722,7 +725,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/07-misc/99-faq.md b/documentation/docs/07-misc/99-faq.md
index b56c27af86..ed5c6277c0 100644
--- a/documentation/docs/07-misc/99-faq.md
+++ b/documentation/docs/07-misc/99-faq.md
@@ -46,7 +46,7 @@ It will show up on hover.
- You can use markdown here.
- You can also use code blocks here.
- Usage:
- ```tsx
+ ```svelte
```
-->
@@ -96,7 +96,7 @@ However, you can use any router library. A lot of people use [page.js](https://g
If you prefer a declarative HTML approach, there's the isomorphic [svelte-routing](https://github.com/EmilTholin/svelte-routing) library and a fork of it called [svelte-navigator](https://github.com/mefechoel/svelte-navigator) containing some additional functionality.
-If you need hash-based routing on the client side, check out [svelte-spa-router](https://github.com/ItalyPaleAle/svelte-spa-router) or [abstract-state-router](https://github.com/TehShrike/abstract-state-router/).
+If you need hash-based routing on the client side, check out the [hash option](https://svelte.dev/docs/kit/configuration#router) in SvelteKit, [svelte-spa-router](https://github.com/ItalyPaleAle/svelte-spa-router), or [abstract-state-router](https://github.com/TehShrike/abstract-state-router/).
[Routify](https://routify.dev) is another filesystem-based router, similar to SvelteKit's router. Version 3 supports Svelte's native SSR.
diff --git a/documentation/docs/07-misc/xx-reactivity-indepth.md b/documentation/docs/07-misc/xx-reactivity-indepth.md
deleted file mode 100644
index b40072552f..0000000000
--- a/documentation/docs/07-misc/xx-reactivity-indepth.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-title: Reactivity in depth
----
-
-- how to think about Runes ("just JavaScript" with added reactivity, what this means for keeping reactivity alive across boundaries)
-- signals
diff --git a/documentation/docs/98-reference/.generated/client-errors.md b/documentation/docs/98-reference/.generated/client-errors.md
index 2c2e0707ea..32348bb781 100644
--- a/documentation/docs/98-reference/.generated/client-errors.md
+++ b/documentation/docs/98-reference/.generated/client-errors.md
@@ -21,7 +21,7 @@ A component is attempting to bind to a non-bindable property `%key%` belonging t
### component_api_changed
```
-%parent% called `%method%` on an instance of %component%, which is no longer valid in Svelte 5
+Calling `%method%` on a component instance (of %component%) is no longer valid in Svelte 5
```
See the [migration guide](/docs/svelte/v5-migration-guide#Components-are-no-longer-classes) for more information.
@@ -122,14 +122,39 @@ Property descriptors defined on `$state` objects must contain `value` and always
Cannot set prototype of `$state` object
```
-### state_unsafe_local_read
+### state_unsafe_mutation
```
-Reading state that was created inside the same derived is forbidden. Consider using `untrack` to read locally created state
+Updating state inside a derived or a template expression is forbidden. If the value should not be reactive, declare it without `$state`
```
-### state_unsafe_mutation
+This error occurs when state is updated while evaluating a `$derived`. You might encounter it while trying to 'derive' two pieces of state in one go:
+
+```svelte
+
+ count++}>{count}
+
+{count} is even: {even}
+{count} is odd: {odd}
```
-Updating state inside a derived or a template expression is forbidden. If the value should not be reactive, declare it without `$state`
+
+This is forbidden because it introduces instability: if `{count} is even: {even}
` is updated before `odd` is recalculated, `even` will be stale. In most cases the solution is to make everything derived:
+
+```js
+let count = 0;
+// ---cut---
+let even = $derived(count % 2 === 0);
+let odd = $derived(!even);
```
+
+If side-effects are unavoidable, use [`$effect`]($effect) instead.
diff --git a/documentation/docs/98-reference/.generated/client-warnings.md b/documentation/docs/98-reference/.generated/client-warnings.md
index 284e9a7c3e..77d1df4cdd 100644
--- a/documentation/docs/98-reference/.generated/client-warnings.md
+++ b/documentation/docs/98-reference/.generated/client-warnings.md
@@ -161,7 +161,7 @@ Tried to unmount a component that was not mounted
### ownership_invalid_binding
```
-%parent% passed a value to %child% with `bind:`, but the value is owned by %owner%. Consider creating a binding between %owner% and %parent%
+%parent% passed property `%prop%` to %child% with `bind:`, but its parent component %owner% did not declare `%prop%` as a binding. Consider creating a binding between %owner% and %parent% (e.g. `bind:%prop%={...}` instead of `%prop%={...}`)
```
Consider three components `GrandParent`, `Parent` and `Child`. If you do ``, inside `GrandParent` pass on the variable via ` ` (note the missing `bind:`) and then do `` inside `Parent`, this warning is thrown.
@@ -171,11 +171,7 @@ To fix it, `bind:` to the value instead of just passing a property (i.e. in this
### ownership_invalid_mutation
```
-Mutating a value outside the component that created it is strongly discouraged. Consider passing values to child components with `bind:`, or use a callback instead
-```
-
-```
-%component% mutated a value owned by %owner%. This is strongly discouraged. Consider passing values to child components with `bind:`, or use a callback instead
+Mutating unbound props (`%name%`, at %location%) is strongly discouraged. Consider using `bind:%prop%={...}` in %parent% (or using a callback) instead
```
Consider the following code:
diff --git a/documentation/docs/98-reference/.generated/compile-errors.md b/documentation/docs/98-reference/.generated/compile-errors.md
index a867cfe88c..6196a85ade 100644
--- a/documentation/docs/98-reference/.generated/compile-errors.md
+++ b/documentation/docs/98-reference/.generated/compile-errors.md
@@ -84,6 +84,12 @@ Attribute values containing `{...}` must be enclosed in quote marks, unless the
`bind:group` can only bind to an Identifier or MemberExpression
```
+### bind_group_invalid_snippet_parameter
+
+```
+Cannot `bind:group` to a snippet parameter
+```
+
### bind_invalid_expression
```
@@ -187,7 +193,7 @@ Cyclical dependency detected: %cycle%
### const_tag_invalid_placement
```
-`{@const}` must be the immediate child of `{#snippet}`, `{#if}`, `{:else if}`, `{:else}`, `{#each}`, `{:then}`, `{:catch}`, `` or ``
+`{@const}` must be the immediate child of `{#snippet}`, `{#if}`, `{:else if}`, `{:else}`, `{#each}`, `{:then}`, `{:catch}`, ``, ``
```
### constant_assignment
@@ -229,7 +235,31 @@ A top-level `:global {...}` block can only contain rules, not declarations
### css_global_block_invalid_list
```
-A `:global` selector cannot be part of a selector list with more than one item
+A `:global` selector cannot be part of a selector list with entries that don't contain `:global`
+```
+
+The following CSS is invalid:
+
+```css
+:global, x {
+ y {
+ color: red;
+ }
+}
+```
+
+This is mixing a `:global` block, which means "everything in here is unscoped", with a scoped selector (`x` in this case). As a result it's not possible to transform the inner selector (`y` in this case) into something that satisfies both requirements. You therefore have to split this up into two selectors:
+
+```css
+:global {
+ y {
+ color: red;
+ }
+}
+
+x y {
+ color: red;
+}
```
### css_global_block_invalid_modifier
@@ -573,7 +603,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
@@ -648,6 +684,12 @@ Cannot access a computed property of a rune
`%name%` is not a valid rune
```
+### rune_invalid_spread
+
+```
+`%rune%` cannot be called with a spread argument
+```
+
### rune_invalid_usage
```
diff --git a/documentation/docs/98-reference/.generated/compile-warnings.md b/documentation/docs/98-reference/.generated/compile-warnings.md
index 57396bd7fd..0e94cbadb2 100644
--- a/documentation/docs/98-reference/.generated/compile-warnings.md
+++ b/documentation/docs/98-reference/.generated/compile-warnings.md
@@ -823,15 +823,16 @@ See [the migration guide](v5-migration-guide#Snippets-instead-of-slots) for more
### state_referenced_locally
```
-State referenced in its own scope will never update. Did you mean to reference it inside a closure?
+This reference only captures the initial value of `%name%`. Did you mean to reference it inside a %type% instead?
```
This warning is thrown when the compiler detects the following:
+
- A reactive variable is declared
-- the variable is reassigned
-- the variable is referenced inside the same scope it is declared and it is a non-reactive context
+- ...and later reassigned...
+- ...and referenced in the same scope
-In this case, the state reassignment will not be noticed by whatever you passed it to. For example, if you pass the state to a function, that function will not notice the updates:
+This 'breaks the link' to the original state declaration. For example, if you pass the state to a function, the function loses access to the state once it is reassigned:
```svelte
diff --git a/documentation/docs/98-reference/.generated/shared-errors.md b/documentation/docs/98-reference/.generated/shared-errors.md
index 0102aafcbc..6c31aaafd0 100644
--- a/documentation/docs/98-reference/.generated/shared-errors.md
+++ b/documentation/docs/98-reference/.generated/shared-errors.md
@@ -30,6 +30,12 @@ This error would be thrown in a setup like this:
Here, `List.svelte` is using `{@render children(item)` which means it expects `Parent.svelte` to use snippets. Instead, `Parent.svelte` uses the deprecated `let:` directive. This combination of APIs is incompatible, hence the error.
+### invalid_snippet_arguments
+
+```
+A snippet function was passed invalid arguments. Snippets should only be instantiated via `{@render ...}`
+```
+
### lifecycle_outside_component
```
@@ -54,6 +60,43 @@ Certain lifecycle methods can only be used during component initialisation. To f
click me
```
+### snippet_without_render_tag
+
+```
+Attempted to render a snippet without a `{@render}` block. This would cause the snippet code to be stringified instead of its content being rendered to the DOM. To fix this, change `{snippet}` to `{@render snippet()}`.
+```
+
+A component throwing this error will look something like this (`children` is not being rendered):
+
+```svelte
+
+
+{children}
+```
+
+...or like this (a parent component is passing a snippet where a non-snippet value is expected):
+
+```svelte
+
+
+ {#snippet label()}
+ Hi!
+ {/snippet}
+
+```
+
+```svelte
+
+
+
+
+{label}
+```
+
### store_invalid_shape
```
diff --git a/documentation/docs/98-reference/21-svelte-reactivity.md b/documentation/docs/98-reference/21-svelte-reactivity.md
index 6857c1dba8..8070331f48 100644
--- a/documentation/docs/98-reference/21-svelte-reactivity.md
+++ b/documentation/docs/98-reference/21-svelte-reactivity.md
@@ -2,24 +2,6 @@
title: svelte/reactivity
---
-Svelte provides reactive versions of various built-ins like `SvelteMap`, `SvelteSet` and `SvelteURL`. These can be imported from `svelte/reactivity` and used just like their native counterparts.
-
-```svelte
-
-
-
-
-
-
-
-
-
-
-
-```
+Svelte provides reactive versions of various built-ins like [`Map`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map), [`Set`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set) and [`URL`](https://developer.mozilla.org/en-US/docs/Web/API/URL) that can be used just like their native counterparts, as well as a handful of additional utilities for handling reactivity.
> MODULE: svelte/reactivity
diff --git a/package.json b/package.json
index 57dc4cdebe..70e85438f0 100644
--- a/package.json
+++ b/package.json
@@ -5,9 +5,9 @@
"private": true,
"type": "module",
"license": "MIT",
- "packageManager": "pnpm@9.4.0",
+ "packageManager": "pnpm@10.4.0",
"engines": {
- "pnpm": "^9.0.0"
+ "pnpm": ">=9.0.0"
},
"repository": {
"type": "git",
@@ -42,8 +42,8 @@
"prettier-plugin-svelte": "^3.1.2",
"svelte": "workspace:^",
"typescript": "^5.5.4",
- "typescript-eslint": "^8.2.0",
+ "typescript-eslint": "^8.24.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 6023c6d4e1..e5681a3ceb 100644
--- a/packages/svelte/CHANGELOG.md
+++ b/packages/svelte/CHANGELOG.md
@@ -1,5 +1,504 @@
# svelte
+## 5.27.2
+
+### Patch Changes
+
+- chore: use pkg.imports for common modules ([#15787](https://github.com/sveltejs/svelte/pull/15787))
+
+## 5.27.1
+
+### Patch Changes
+
+- chore: default params for html blocks ([#15778](https://github.com/sveltejs/svelte/pull/15778))
+
+- fix: correct suggested type for custom events without detail ([#15763](https://github.com/sveltejs/svelte/pull/15763))
+
+- fix: Throw on unrendered snippets in `dev` ([#15766](https://github.com/sveltejs/svelte/pull/15766))
+
+- fix: avoid unnecessary read version increments ([#15777](https://github.com/sveltejs/svelte/pull/15777))
+
+## 5.27.0
+
+### Minor Changes
+
+- feat: partially evaluate certain expressions ([#15494](https://github.com/sveltejs/svelte/pull/15494))
+
+### Patch Changes
+
+- fix: relax `:global` selector list validation ([#15762](https://github.com/sveltejs/svelte/pull/15762))
+
+## 5.26.3
+
+### Patch Changes
+
+- fix: correctly validate head snippets on the server ([#15755](https://github.com/sveltejs/svelte/pull/15755))
+
+- fix: ignore mutation validation for props that are not proxies in more cases ([#15759](https://github.com/sveltejs/svelte/pull/15759))
+
+- fix: allow self-closing tags within math namespace ([#15761](https://github.com/sveltejs/svelte/pull/15761))
+
+## 5.26.2
+
+### Patch Changes
+
+- fix: correctly validate `undefined` snippet params with default value ([#15750](https://github.com/sveltejs/svelte/pull/15750))
+
+## 5.26.1
+
+### Patch Changes
+
+- fix: update `state_referenced_locally` message ([#15733](https://github.com/sveltejs/svelte/pull/15733))
+
+## 5.26.0
+
+### Minor Changes
+
+- feat: add `css.hasGlobal` to `compile` output ([#15450](https://github.com/sveltejs/svelte/pull/15450))
+
+### Patch Changes
+
+- fix: add snippet argument validation in dev ([#15521](https://github.com/sveltejs/svelte/pull/15521))
+
+## 5.25.12
+
+### Patch Changes
+
+- fix: improve internal_set versioning mechanic ([#15724](https://github.com/sveltejs/svelte/pull/15724))
+
+- fix: don't transform reassigned state in labeled statement in `$derived` ([#15725](https://github.com/sveltejs/svelte/pull/15725))
+
+## 5.25.11
+
+### Patch Changes
+
+- fix: handle hydration mismatches in await blocks ([#15708](https://github.com/sveltejs/svelte/pull/15708))
+
+- fix: prevent ownership warnings if the fallback of a bindable is used ([#15720](https://github.com/sveltejs/svelte/pull/15720))
+
+## 5.25.10
+
+### Patch Changes
+
+- fix: set deriveds as `CLEAN` if they are assigned to ([#15592](https://github.com/sveltejs/svelte/pull/15592))
+
+- fix: better scope `:global()` with nesting selector `&` ([#15671](https://github.com/sveltejs/svelte/pull/15671))
+
+## 5.25.9
+
+### Patch Changes
+
+- fix: allow `$.state` and `$.derived` to be treeshaken ([#15702](https://github.com/sveltejs/svelte/pull/15702))
+
+- fix: rework binding ownership validation ([#15678](https://github.com/sveltejs/svelte/pull/15678))
+
+## 5.25.8
+
+### Patch Changes
+
+- fix: address untracked_writes memory leak ([#15694](https://github.com/sveltejs/svelte/pull/15694))
+
+## 5.25.7
+
+### Patch Changes
+
+- fix: ensure clearing of old values happens independent of root flushes ([#15664](https://github.com/sveltejs/svelte/pull/15664))
+
+## 5.25.6
+
+### Patch Changes
+
+- fix: ignore generic type arguments while creating AST ([#15659](https://github.com/sveltejs/svelte/pull/15659))
+
+- fix: better consider component and its snippets during css pruning ([#15630](https://github.com/sveltejs/svelte/pull/15630))
+
+## 5.25.5
+
+### Patch Changes
+
+- fix: add setters to `$derived` class properties ([#15628](https://github.com/sveltejs/svelte/pull/15628))
+
+- fix: silence assignment warning on more function bindings ([#15644](https://github.com/sveltejs/svelte/pull/15644))
+
+- fix: make sure CSS is preserved during SSR with bindings ([#15645](https://github.com/sveltejs/svelte/pull/15645))
+
+## 5.25.4
+
+### Patch Changes
+
+- fix: support TS type assertions ([#15642](https://github.com/sveltejs/svelte/pull/15642))
+
+- fix: ensure `undefined` class still applies scoping class, if necessary ([#15643](https://github.com/sveltejs/svelte/pull/15643))
+
+## 5.25.3
+
+### Patch Changes
+
+- fix: prevent state runes from being called with spread ([#15585](https://github.com/sveltejs/svelte/pull/15585))
+
+## 5.25.2
+
+### Patch Changes
+
+- feat: migrate reassigned deriveds to `$derived` ([#15581](https://github.com/sveltejs/svelte/pull/15581))
+
+## 5.25.1
+
+### Patch Changes
+
+- fix: prevent dev server from throwing errors when attempting to retrieve the proxied value of an iframe's contentWindow ([#15577](https://github.com/sveltejs/svelte/pull/15577))
+
+## 5.25.0
+
+### Minor Changes
+
+- feat: make deriveds writable ([#15570](https://github.com/sveltejs/svelte/pull/15570))
+
+## 5.24.1
+
+### Patch Changes
+
+- fix: use `get` in constructor for deriveds ([#15300](https://github.com/sveltejs/svelte/pull/15300))
+
+- fix: ensure toStore root effect is connected to correct parent effect ([#15574](https://github.com/sveltejs/svelte/pull/15574))
+
+## 5.24.0
+
+### Minor Changes
+
+- feat: allow state created in deriveds/effects to be written/read locally without self-invalidation ([#15553](https://github.com/sveltejs/svelte/pull/15553))
+
+### Patch Changes
+
+- fix: check if DOM prototypes are extensible ([#15569](https://github.com/sveltejs/svelte/pull/15569))
+
+- Keep inlined trailing JSDoc comments of properties when running svelte-migrate ([#15567](https://github.com/sveltejs/svelte/pull/15567))
+
+- fix: simplify set calls for proxyable values ([#15548](https://github.com/sveltejs/svelte/pull/15548))
+
+- fix: don't depend on deriveds created inside the current reaction ([#15564](https://github.com/sveltejs/svelte/pull/15564))
+
+## 5.23.2
+
+### Patch Changes
+
+- fix: don't hoist listeners that access non hoistable snippets ([#15534](https://github.com/sveltejs/svelte/pull/15534))
+
+## 5.23.1
+
+### Patch Changes
+
+- fix: invalidate parent effects when child effects update parent dependencies ([#15506](https://github.com/sveltejs/svelte/pull/15506))
+
+- fix: correctly match `:has()` selector during css pruning ([#15277](https://github.com/sveltejs/svelte/pull/15277))
+
+- fix: replace `undefined` with `void 0` to avoid edge case ([#15511](https://github.com/sveltejs/svelte/pull/15511))
+
+- fix: allow global-like pseudo-selectors refinement ([#15313](https://github.com/sveltejs/svelte/pull/15313))
+
+- chore: don't distribute unused types definitions ([#15473](https://github.com/sveltejs/svelte/pull/15473))
+
+- fix: add `files` and `group` to HTMLInputAttributes in elements.d.ts ([#15492](https://github.com/sveltejs/svelte/pull/15492))
+
+- fix: throw rune_invalid_arguments_length when $state.raw() is used with more than 1 arg ([#15516](https://github.com/sveltejs/svelte/pull/15516))
+
+## 5.23.0
+
+### Minor Changes
+
+- fix: make values consistent between effects and their cleanup functions ([#15469](https://github.com/sveltejs/svelte/pull/15469))
+
+## 5.22.6
+
+### Patch Changes
+
+- fix: skip `log_if_contains_state` if only logging literals ([#15468](https://github.com/sveltejs/svelte/pull/15468))
+
+- fix: Add `closedby` property to HTMLDialogAttributes type ([#15458](https://github.com/sveltejs/svelte/pull/15458))
+
+- fix: null and warnings for local handlers ([#15460](https://github.com/sveltejs/svelte/pull/15460))
+
+## 5.22.5
+
+### Patch Changes
+
+- fix: memoize `clsx` calls ([#15456](https://github.com/sveltejs/svelte/pull/15456))
+
+- fix: respect `svelte-ignore hydration_attribute_changed` on elements with spread attributes ([#15443](https://github.com/sveltejs/svelte/pull/15443))
+
+- fix: always use `setAttribute` when setting `style` ([#15323](https://github.com/sveltejs/svelte/pull/15323))
+
+- fix: make `style:` directive and CSS handling more robust ([#15418](https://github.com/sveltejs/svelte/pull/15418))
+
+## 5.22.4
+
+### Patch Changes
+
+- fix: never deduplicate expressions in templates ([#15451](https://github.com/sveltejs/svelte/pull/15451))
+
+## 5.22.3
+
+### Patch Changes
+
+- fix: run effect roots in tree order ([#15446](https://github.com/sveltejs/svelte/pull/15446))
+
+## 5.22.2
+
+### Patch Changes
+
+- fix: correctly set `is_updating` before flushing root effects ([#15442](https://github.com/sveltejs/svelte/pull/15442))
+
+## 5.22.1
+
+### Patch Changes
+
+- chore: switch acorn-typescript plugin ([#15393](https://github.com/sveltejs/svelte/pull/15393))
+
+## 5.22.0
+
+### Minor Changes
+
+- feat: Add `idPrefix` option to `render` ([#15428](https://github.com/sveltejs/svelte/pull/15428))
+
+### Patch Changes
+
+- fix: make dialog element and role interactive ([#15429](https://github.com/sveltejs/svelte/pull/15429))
+
+## 5.21.0
+
+### Minor Changes
+
+- chore: Reduce hydration comment for {:else if} ([#15250](https://github.com/sveltejs/svelte/pull/15250))
+
+### Patch Changes
+
+- fix: disallow `bind:group` to snippet parameters ([#15401](https://github.com/sveltejs/svelte/pull/15401))
+
+## 5.20.5
+
+### Patch Changes
+
+- fix: allow double hyphen css selector names ([#15384](https://github.com/sveltejs/svelte/pull/15384))
+
+- fix: class:directive not working with $restProps #15386 ([#15389](https://github.com/sveltejs/svelte/pull/15389))
+ fix: spread add an useless cssHash on non-scoped element
+
+- fix: catch error on @const tag in svelte:boundary in DEV mode ([#15369](https://github.com/sveltejs/svelte/pull/15369))
+
+- fix: allow for duplicate `var` declarations ([#15382](https://github.com/sveltejs/svelte/pull/15382))
+
+- fix : bug "$0 is not defined" on svelte:element with a function call on class ([#15396](https://github.com/sveltejs/svelte/pull/15396))
+
+## 5.20.4
+
+### Patch Changes
+
+- fix: update types and inline docs for flushSync ([#15348](https://github.com/sveltejs/svelte/pull/15348))
+
+## 5.20.3
+
+### Patch Changes
+
+- fix: allow `@const` inside `#key` ([#15377](https://github.com/sveltejs/svelte/pull/15377))
+
+- fix: remove unnecessary `?? ''` on some expressions ([#15287](https://github.com/sveltejs/svelte/pull/15287))
+
+- fix: correctly override class attributes with class directives ([#15352](https://github.com/sveltejs/svelte/pull/15352))
+
+## 5.20.2
+
+### Patch Changes
+
+- chore: remove unused `options.uid` in `render` ([#15302](https://github.com/sveltejs/svelte/pull/15302))
+
+- fix: do not warn for `binding_property_non_reactive` if binding is a store in an each ([#15318](https://github.com/sveltejs/svelte/pull/15318))
+
+- fix: prevent writable store value from becoming a proxy when reassigning using $-prefix ([#15283](https://github.com/sveltejs/svelte/pull/15283))
+
+- fix: `muted` reactive without `bind` and select/autofocus attributes working with function calls ([#15326](https://github.com/sveltejs/svelte/pull/15326))
+
+- fix: ensure input elements and elements with `dir` attribute are marked as non-static ([#15259](https://github.com/sveltejs/svelte/pull/15259))
+
+- fix: fire delegated events on target even it was disabled in the meantime ([#15319](https://github.com/sveltejs/svelte/pull/15319))
+
+## 5.20.1
+
+### Patch Changes
+
+- fix: ensure AST analysis on `svelte.js` modules succeeds ([#15297](https://github.com/sveltejs/svelte/pull/15297))
+
+- fix: ignore typescript abstract methods ([#15267](https://github.com/sveltejs/svelte/pull/15267))
+
+- fix: correctly ssr component in `svelte:head` with `$props.id()` or `css='injected'` ([#15291](https://github.com/sveltejs/svelte/pull/15291))
+
+## 5.20.0
+
+### Minor Changes
+
+- feat: SSR-safe ID generation with `$props.id()` ([#15185](https://github.com/sveltejs/svelte/pull/15185))
+
+### Patch Changes
+
+- fix: take private and public into account for `constant_assignment` of derived state ([#15276](https://github.com/sveltejs/svelte/pull/15276))
+
+- fix: value/checked not correctly set using spread ([#15239](https://github.com/sveltejs/svelte/pull/15239))
+
+- chore: tweak effect self invalidation logic, run transition dispatches without reactive context ([#15275](https://github.com/sveltejs/svelte/pull/15275))
+
+- fix: use `importNode` to clone templates for Firefox ([#15272](https://github.com/sveltejs/svelte/pull/15272))
+
+- fix: recurse into `$derived` for ownership validation ([#15166](https://github.com/sveltejs/svelte/pull/15166))
+
+## 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
+
+- fix: omit unnecessary nullish coallescing in template expressions ([#15056](https://github.com/sveltejs/svelte/pull/15056))
+
+- fix: more efficient template effect grouping ([#15050](https://github.com/sveltejs/svelte/pull/15050))
+
+- fix: ensure untrack correctly retains the active reaction ([#15065](https://github.com/sveltejs/svelte/pull/15065))
+
+- fix: initialize `files` bind on hydration ([#15059](https://github.com/sveltejs/svelte/pull/15059))
+
+## 5.19.0
+
+### Minor Changes
+
+- feat: Expose `ClassValue` from `svelte/elements` ([#15035](https://github.com/sveltejs/svelte/pull/15035))
+
+### Patch Changes
+
+- fix: create fewer deriveds for concatenated strings ([#15041](https://github.com/sveltejs/svelte/pull/15041))
+
+- fix: correctly parse leading comments in function binding ([#15020](https://github.com/sveltejs/svelte/pull/15020))
+
+## 5.18.0
+
+### Minor Changes
+
+- feat: allow `` elements to contain any child ([#15007](https://github.com/sveltejs/svelte/pull/15007))
+
+### Patch Changes
+
+- fix: ensure resume effects are scheduled in topological order ([#15012](https://github.com/sveltejs/svelte/pull/15012))
+
+- fix: bump esrap ([#15015](https://github.com/sveltejs/svelte/pull/15015))
+
+- fix: remove listener on `bind_current_time` teardown ([#15013](https://github.com/sveltejs/svelte/pull/15013))
+
+## 5.17.5
+
+### Patch Changes
+
+- feat: allow const tag inside `svelte:boundary` ([#14993](https://github.com/sveltejs/svelte/pull/14993))
+
+- fix: ensure signal write invalidation within effects is consistent ([#14989](https://github.com/sveltejs/svelte/pull/14989))
+
+## 5.17.4
+
+### Patch Changes
+
+- fix: never consider inert boundary effects ([#14999](https://github.com/sveltejs/svelte/pull/14999))
+
+- fix: store access on component destroy ([#14968](https://github.com/sveltejs/svelte/pull/14968))
+
+- fix: correctly transform `pre` with no content ([#14973](https://github.com/sveltejs/svelte/pull/14973))
+
+- fix: wrap each block expression in derived to encapsulate effects ([#14967](https://github.com/sveltejs/svelte/pull/14967))
+
## 5.17.3
### Patch Changes
diff --git a/packages/svelte/README.md b/packages/svelte/README.md
index 23e35fd4a7..c37617da30 100644
--- a/packages/svelte/README.md
+++ b/packages/svelte/README.md
@@ -1,4 +1,9 @@
-[](https://svelte.dev)
+
+
+
+
+
+
[](https://www.npmjs.com/package/svelte) [](LICENSE.md) [](https://svelte.dev/chat)
diff --git a/packages/svelte/elements.d.ts b/packages/svelte/elements.d.ts
index 604403f0a2..99d87b4c09 100644
--- a/packages/svelte/elements.d.ts
+++ b/packages/svelte/elements.d.ts
@@ -741,7 +741,7 @@ export interface HTMLAttributes extends AriaAttributes, D
accesskey?: string | undefined | null;
autocapitalize?: 'characters' | 'off' | 'on' | 'none' | 'sentences' | 'words' | undefined | null;
autofocus?: boolean | undefined | null;
- class?: string | import('clsx').ClassArray | import('clsx').ClassDictionary | undefined | null;
+ class?: ClassValue | undefined | null;
contenteditable?: Booleanish | 'inherit' | 'plaintext-only' | undefined | null;
contextmenu?: string | undefined | null;
dir?: 'ltr' | 'rtl' | 'auto' | undefined | null;
@@ -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;
@@ -956,6 +957,7 @@ export interface HTMLDelAttributes extends HTMLAttributes {
export interface HTMLDialogAttributes extends HTMLAttributes {
open?: boolean | undefined | null;
+ closedby?: 'any' | 'closerequest' | 'none' | undefined | null;
}
export interface HTMLEmbedAttributes extends HTMLAttributes {
@@ -1074,6 +1076,7 @@ export interface HTMLInputAttributes extends HTMLAttributes {
checked?: boolean | undefined | null;
dirname?: string | undefined | null;
disabled?: boolean | undefined | null;
+ files?: FileList | undefined | null;
form?: string | undefined | null;
formaction?: string | undefined | null;
formenctype?:
@@ -1085,6 +1088,7 @@ export interface HTMLInputAttributes extends HTMLAttributes {
formmethod?: 'dialog' | 'get' | 'post' | 'DIALOG' | 'GET' | 'POST' | undefined | null;
formnovalidate?: boolean | undefined | null;
formtarget?: string | undefined | null;
+ group?: any | undefined | null;
height?: number | string | undefined | null;
indeterminate?: boolean | undefined | null;
list?: string | undefined | null;
@@ -1522,7 +1526,7 @@ export interface SvelteWindowAttributes extends HTMLAttributes {
export interface SVGAttributes extends AriaAttributes, DOMAttributes {
// Attributes which also defined in HTMLAttributes
className?: string | undefined | null;
- class?: string | import('clsx').ClassArray | import('clsx').ClassDictionary | undefined | null;
+ class?: ClassValue | undefined | null;
color?: string | undefined | null;
height?: number | string | undefined | null;
id?: string | undefined | null;
@@ -2059,3 +2063,5 @@ export interface SvelteHTMLElements {
[name: string]: { [name: string]: any };
}
+
+export type ClassValue = string | import('clsx').ClassArray | import('clsx').ClassDictionary;
diff --git a/packages/svelte/messages/client-errors/errors.md b/packages/svelte/messages/client-errors/errors.md
index ce1f222c63..c4e68f8fee 100644
--- a/packages/svelte/messages/client-errors/errors.md
+++ b/packages/svelte/messages/client-errors/errors.md
@@ -12,7 +12,7 @@
## component_api_changed
-> %parent% called `%method%` on an instance of %component%, which is no longer valid in Svelte 5
+> Calling `%method%` on a component instance (of %component%) is no longer valid in Svelte 5
See the [migration guide](/docs/svelte/v5-migration-guide#Components-are-no-longer-classes) for more information.
@@ -80,10 +80,37 @@ See the [migration guide](/docs/svelte/v5-migration-guide#Components-are-no-long
> Cannot set prototype of `$state` object
-## state_unsafe_local_read
-
-> Reading state that was created inside the same derived is forbidden. Consider using `untrack` to read locally created state
-
## state_unsafe_mutation
> Updating state inside a derived or a template expression is forbidden. If the value should not be reactive, declare it without `$state`
+
+This error occurs when state is updated while evaluating a `$derived`. You might encounter it while trying to 'derive' two pieces of state in one go:
+
+```svelte
+
+
+ count++}>{count}
+
+{count} is even: {even}
+{count} is odd: {odd}
+```
+
+This is forbidden because it introduces instability: if `{count} is even: {even}
` is updated before `odd` is recalculated, `even` will be stale. In most cases the solution is to make everything derived:
+
+```js
+let count = 0;
+// ---cut---
+let even = $derived(count % 2 === 0);
+let odd = $derived(!even);
+```
+
+If side-effects are unavoidable, use [`$effect`]($effect) instead.
diff --git a/packages/svelte/messages/client-warnings/warnings.md b/packages/svelte/messages/client-warnings/warnings.md
index 943cf6f01f..f8e9ebd8a0 100644
--- a/packages/svelte/messages/client-warnings/warnings.md
+++ b/packages/svelte/messages/client-warnings/warnings.md
@@ -132,7 +132,7 @@ During development, this error is often preceeded by a `console.error` detailing
## ownership_invalid_binding
-> %parent% passed a value to %child% with `bind:`, but the value is owned by %owner%. Consider creating a binding between %owner% and %parent%
+> %parent% passed property `%prop%` to %child% with `bind:`, but its parent component %owner% did not declare `%prop%` as a binding. Consider creating a binding between %owner% and %parent% (e.g. `bind:%prop%={...}` instead of `%prop%={...}`)
Consider three components `GrandParent`, `Parent` and `Child`. If you do ``, inside `GrandParent` pass on the variable via ` ` (note the missing `bind:`) and then do `` inside `Parent`, this warning is thrown.
@@ -140,9 +140,7 @@ To fix it, `bind:` to the value instead of just passing a property (i.e. in this
## ownership_invalid_mutation
-> Mutating a value outside the component that created it is strongly discouraged. Consider passing values to child components with `bind:`, or use a callback instead
-
-> %component% mutated a value owned by %owner%. This is strongly discouraged. Consider passing values to child components with `bind:`, or use a callback instead
+> Mutating unbound props (`%name%`, at %location%) is strongly discouraged. Consider using `bind:%prop%={...}` in %parent% (or using a callback) instead
Consider the following code:
diff --git a/packages/svelte/messages/compile-errors/script.md b/packages/svelte/messages/compile-errors/script.md
index 0aa6fbed90..aabcbeae48 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
@@ -158,6 +162,10 @@ This turned out to be buggy and unpredictable, particularly when working with de
> `%name%` is not a valid rune
+## rune_invalid_spread
+
+> `%rune%` cannot be called with a spread argument
+
## rune_invalid_usage
> Cannot use `%rune%` rune in non-runes mode
diff --git a/packages/svelte/messages/compile-errors/style.md b/packages/svelte/messages/compile-errors/style.md
index 1e1ab45e8c..f08a2156a3 100644
--- a/packages/svelte/messages/compile-errors/style.md
+++ b/packages/svelte/messages/compile-errors/style.md
@@ -16,7 +16,31 @@
## css_global_block_invalid_list
-> A `:global` selector cannot be part of a selector list with more than one item
+> A `:global` selector cannot be part of a selector list with entries that don't contain `:global`
+
+The following CSS is invalid:
+
+```css
+:global, x {
+ y {
+ color: red;
+ }
+}
+```
+
+This is mixing a `:global` block, which means "everything in here is unscoped", with a scoped selector (`x` in this case). As a result it's not possible to transform the inner selector (`y` in this case) into something that satisfies both requirements. You therefore have to split this up into two selectors:
+
+```css
+:global {
+ y {
+ color: red;
+ }
+}
+
+x y {
+ color: red;
+}
+```
## css_global_block_invalid_modifier
diff --git a/packages/svelte/messages/compile-errors/template.md b/packages/svelte/messages/compile-errors/template.md
index 287178ef87..0569f63ad3 100644
--- a/packages/svelte/messages/compile-errors/template.md
+++ b/packages/svelte/messages/compile-errors/template.md
@@ -54,6 +54,10 @@
> `bind:group` can only bind to an Identifier or MemberExpression
+## bind_group_invalid_snippet_parameter
+
+> Cannot `bind:group` to a snippet parameter
+
## bind_invalid_expression
> Can only bind to an Identifier or MemberExpression or a `{get, set}` pair
@@ -118,7 +122,7 @@
## const_tag_invalid_placement
-> `{@const}` must be the immediate child of `{#snippet}`, `{#if}`, `{:else if}`, `{:else}`, `{#each}`, `{:then}`, `{:catch}`, `` or ``
+> `{@const}` must be the immediate child of `{#snippet}`, `{#if}`, `{:else if}`, `{:else}`, `{#each}`, `{:then}`, `{:catch}`, ``, ``
## debug_tag_invalid_arguments
diff --git a/packages/svelte/messages/compile-warnings/script.md b/packages/svelte/messages/compile-warnings/script.md
index 8c32fb7082..6603759156 100644
--- a/packages/svelte/messages/compile-warnings/script.md
+++ b/packages/svelte/messages/compile-warnings/script.md
@@ -54,14 +54,15 @@ To fix this, wrap your variable declaration with `$state`.
## state_referenced_locally
-> State referenced in its own scope will never update. Did you mean to reference it inside a closure?
+> This reference only captures the initial value of `%name%`. Did you mean to reference it inside a %type% instead?
This warning is thrown when the compiler detects the following:
+
- A reactive variable is declared
-- the variable is reassigned
-- the variable is referenced inside the same scope it is declared and it is a non-reactive context
+- ...and later reassigned...
+- ...and referenced in the same scope
-In this case, the state reassignment will not be noticed by whatever you passed it to. For example, if you pass the state to a function, that function will not notice the updates:
+This 'breaks the link' to the original state declaration. For example, if you pass the state to a function, the function loses access to the state once it is reassigned:
```svelte
diff --git a/packages/svelte/messages/shared-errors/errors.md b/packages/svelte/messages/shared-errors/errors.md
index 8b4c61303a..4b4d332202 100644
--- a/packages/svelte/messages/shared-errors/errors.md
+++ b/packages/svelte/messages/shared-errors/errors.md
@@ -26,6 +26,10 @@ This error would be thrown in a setup like this:
Here, `List.svelte` is using `{@render children(item)` which means it expects `Parent.svelte` to use snippets. Instead, `Parent.svelte` uses the deprecated `let:` directive. This combination of APIs is incompatible, hence the error.
+## invalid_snippet_arguments
+
+> A snippet function was passed invalid arguments. Snippets should only be instantiated via `{@render ...}`
+
## lifecycle_outside_component
> `%name%(...)` can only be used during component initialisation
@@ -48,6 +52,41 @@ Certain lifecycle methods can only be used during component initialisation. To f
click me
```
+## snippet_without_render_tag
+
+> Attempted to render a snippet without a `{@render}` block. This would cause the snippet code to be stringified instead of its content being rendered to the DOM. To fix this, change `{snippet}` to `{@render snippet()}`.
+
+A component throwing this error will look something like this (`children` is not being rendered):
+
+```svelte
+
+
+{children}
+```
+
+...or like this (a parent component is passing a snippet where a non-snippet value is expected):
+
+```svelte
+
+
+ {#snippet label()}
+ Hi!
+ {/snippet}
+
+```
+
+```svelte
+
+
+
+
+{label}
+```
+
## store_invalid_shape
> `%name%` is not a store with a `subscribe` method
diff --git a/packages/svelte/package.json b/packages/svelte/package.json
index 47aec9ee93..dc6c57b1a2 100644
--- a/packages/svelte/package.json
+++ b/packages/svelte/package.json
@@ -2,18 +2,19 @@
"name": "svelte",
"description": "Cybernetically enhanced web apps",
"license": "MIT",
- "version": "5.17.3",
+ "version": "5.27.2",
"type": "module",
"types": "./types/index.d.ts",
"engines": {
"node": ">=18"
},
"files": [
+ "*.d.ts",
"src",
"!src/**/*.test.*",
+ "!src/**/*.d.ts",
"types",
"compiler",
- "*.d.ts",
"README.md"
],
"module": "src/index-client.js",
@@ -102,6 +103,17 @@
"default": "./src/events/index.js"
}
},
+ "imports": {
+ "#client": "./src/internal/client/types.d.ts",
+ "#client/constants": "./src/internal/client/constants.js",
+ "#compiler": {
+ "types": "./src/compiler/private.d.ts",
+ "default": "./src/compiler/index.js"
+ },
+ "#compiler/builders": "./src/compiler/utils/builders.js",
+ "#server": "./src/internal/server/types.d.ts",
+ "#shared": "./src/internal/shared/types.d.ts"
+ },
"repository": {
"type": "git",
"url": "git+https://github.com/sveltejs/svelte.git",
@@ -137,25 +149,25 @@
"@rollup/plugin-virtual": "^3.0.2",
"@types/aria-query": "^5.0.4",
"@types/node": "^20.11.5",
- "dts-buddy": "^0.5.3",
+ "dts-buddy": "^0.5.5",
"esbuild": "^0.21.5",
"rollup": "^4.22.4",
"source-map": "^0.7.4",
- "tiny-glob": "^0.2.9",
+ "tinyglobby": "^0.2.12",
"typescript": "^5.5.4",
- "vitest": "^2.0.5"
+ "vitest": "^2.1.9"
},
"dependencies": {
"@ampproject/remapping": "^2.3.0",
"@jridgewell/sourcemap-codec": "^1.5.0",
"@types/estree": "^1.0.5",
"acorn": "^8.12.1",
- "acorn-typescript": "^1.4.13",
+ "@sveltejs/acorn-typescript": "^1.0.5",
"aria-query": "^5.3.1",
"axobject-query": "^4.1.0",
"clsx": "^2.1.1",
"esm-env": "^1.2.1",
- "esrap": "^1.4.2",
+ "esrap": "^1.4.6",
"is-reference": "^3.0.3",
"locate-character": "^3.0.0",
"magic-string": "^0.30.11",
diff --git a/packages/svelte/scripts/generate-types.js b/packages/svelte/scripts/generate-types.js
index d44afe8205..377fca4343 100644
--- a/packages/svelte/scripts/generate-types.js
+++ b/packages/svelte/scripts/generate-types.js
@@ -24,7 +24,12 @@ await createBundle({
output: `${dir}/types/index.d.ts`,
compilerOptions: {
// so that types/properties with `@internal` (and its dependencies) are removed from the output
- stripInternal: true
+ stripInternal: true,
+ paths: Object.fromEntries(
+ Object.entries(pkg.imports).map(([key, value]) => {
+ return [key, [value.types ?? value.default ?? value]];
+ })
+ )
},
modules: {
[pkg.name]: `${dir}/src/index.d.ts`,
diff --git a/packages/svelte/src/ambient.d.ts b/packages/svelte/src/ambient.d.ts
index fbcecba8e4..a1484718cc 100644
--- a/packages/svelte/src/ambient.d.ts
+++ b/packages/svelte/src/ambient.d.ts
@@ -339,6 +339,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 da038af430..aa328764e1 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`);
}
/**
@@ -373,6 +383,16 @@ export function rune_invalid_name(node, name) {
e(node, 'rune_invalid_name', `\`${name}\` is not a valid rune\nhttps://svelte.dev/e/rune_invalid_name`);
}
+/**
+ * `%rune%` cannot be called with a spread argument
+ * @param {null | number | NodeLike} node
+ * @param {string} rune
+ * @returns {never}
+ */
+export function rune_invalid_spread(node, rune) {
+ e(node, 'rune_invalid_spread', `\`${rune}\` cannot be called with a spread argument\nhttps://svelte.dev/e/rune_invalid_spread`);
+}
+
/**
* Cannot use `%rune%` rune in non-runes mode
* @param {null | number | NodeLike} node
@@ -535,12 +555,12 @@ export function css_global_block_invalid_declaration(node) {
}
/**
- * A `:global` selector cannot be part of a selector list with more than one item
+ * A `:global` selector cannot be part of a selector list with entries that don't contain `:global`
* @param {null | number | NodeLike} node
* @returns {never}
*/
export function css_global_block_invalid_list(node) {
- e(node, 'css_global_block_invalid_list', `A \`:global\` selector cannot be part of a selector list with more than one item\nhttps://svelte.dev/e/css_global_block_invalid_list`);
+ e(node, 'css_global_block_invalid_list', `A \`:global\` selector cannot be part of a selector list with entries that don't contain \`:global\`\nhttps://svelte.dev/e/css_global_block_invalid_list`);
}
/**
@@ -742,6 +762,15 @@ export function bind_group_invalid_expression(node) {
e(node, 'bind_group_invalid_expression', `\`bind:group\` can only bind to an Identifier or MemberExpression\nhttps://svelte.dev/e/bind_group_invalid_expression`);
}
+/**
+ * Cannot `bind:group` to a snippet parameter
+ * @param {null | number | NodeLike} node
+ * @returns {never}
+ */
+export function bind_group_invalid_snippet_parameter(node) {
+ e(node, 'bind_group_invalid_snippet_parameter', `Cannot \`bind:group\` to a snippet parameter\nhttps://svelte.dev/e/bind_group_invalid_snippet_parameter`);
+}
+
/**
* Can only bind to an Identifier or MemberExpression or a `{get, set}` pair
* @param {null | number | NodeLike} node
@@ -888,12 +917,12 @@ export function const_tag_invalid_expression(node) {
}
/**
- * `{@const}` must be the immediate child of `{#snippet}`, `{#if}`, `{:else if}`, `{:else}`, `{#each}`, `{:then}`, `{:catch}`, `` or ``
+ * `{@const}` must be the immediate child of `{#snippet}`, `{#if}`, `{:else if}`, `{:else}`, `{#each}`, `{:then}`, `{:catch}`, ``, ``
* @param {null | number | NodeLike} node
* @returns {never}
*/
export function const_tag_invalid_placement(node) {
- e(node, 'const_tag_invalid_placement', `\`{@const}\` must be the immediate child of \`{#snippet}\`, \`{#if}\`, \`{:else if}\`, \`{:else}\`, \`{#each}\`, \`{:then}\`, \`{:catch}\`, \`\` or \`\`\nhttps://svelte.dev/e/const_tag_invalid_placement`);
+ e(node, 'const_tag_invalid_placement', `\`{@const}\` must be the immediate child of \`{#snippet}\`, \`{#if}\`, \`{:else if}\`, \`{:else}\`, \`{#each}\`, \`{:then}\`, \`{:catch}\`, \`\`, \`\`\nhttps://svelte.dev/e/const_tag_invalid_placement`);
}
/**
diff --git a/packages/svelte/src/compiler/migrate/index.js b/packages/svelte/src/compiler/migrate/index.js
index 1bb7a69a20..75a9a64905 100644
--- a/packages/svelte/src/compiler/migrate/index.js
+++ b/packages/svelte/src/compiler/migrate/index.js
@@ -1,7 +1,7 @@
/** @import { VariableDeclarator, Node, Identifier, AssignmentExpression, LabeledStatement, ExpressionStatement } from 'estree' */
/** @import { Visitors } from 'zimmerframe' */
/** @import { ComponentAnalysis } from '../phases/types.js' */
-/** @import { Scope, ScopeRoot } from '../phases/scope.js' */
+/** @import { Scope } from '../phases/scope.js' */
/** @import { AST, Binding, ValidatedCompileOptions } from '#compiler' */
import MagicString from 'magic-string';
import { walk } from 'zimmerframe';
@@ -944,54 +944,53 @@ const instance_script = {
node.body.type === 'ExpressionStatement' &&
node.body.expression.type === 'AssignmentExpression'
) {
- const ids = extract_identifiers(node.body.expression.left);
- const [, expression_ids] = extract_all_identifiers_from_expression(
- node.body.expression.right
- );
- const bindings = ids.map((id) => state.scope.get(id.name));
- const reassigned_bindings = bindings.filter((b) => b?.reassigned);
+ const { left, right } = node.body.expression;
- if (
- reassigned_bindings.length === 0 &&
- !bindings.some((b) => b?.kind === 'store_sub') &&
- node.body.expression.left.type !== 'MemberExpression'
- ) {
- let { start, end } = /** @type {{ start: number, end: number }} */ (
- node.body.expression.right
- );
+ const ids = extract_identifiers(left);
+ const [, expression_ids] = extract_all_identifiers_from_expression(right);
+ const bindings = ids.map((id) => /** @type {Binding} */ (state.scope.get(id.name)));
- check_rune_binding('derived');
+ if (bindings.every((b) => b.kind === 'legacy_reactive')) {
+ if (
+ right.type !== 'Literal' &&
+ bindings.every((b) => b.kind !== 'store_sub') &&
+ left.type !== 'MemberExpression'
+ ) {
+ let { start, end } = /** @type {{ start: number, end: number }} */ (right);
- // $derived
- state.str.update(
- /** @type {number} */ (node.start),
- /** @type {number} */ (node.body.expression.start),
- 'let '
- );
+ check_rune_binding('derived');
- if (node.body.expression.right.type === 'SequenceExpression') {
- while (state.str.original[start] !== '(') start -= 1;
- while (state.str.original[end - 1] !== ')') end += 1;
- }
+ // $derived
+ state.str.update(
+ /** @type {number} */ (node.start),
+ /** @type {number} */ (node.body.expression.start),
+ 'let '
+ );
- state.str.prependRight(start, `$derived(`);
+ if (right.type === 'SequenceExpression') {
+ while (state.str.original[start] !== '(') start -= 1;
+ while (state.str.original[end - 1] !== ')') end += 1;
+ }
+
+ state.str.prependRight(start, `$derived(`);
+
+ // in a case like `$: ({ a } = b())`, there's already a trailing parenthesis.
+ // otherwise, we need to add one
+ if (state.str.original[/** @type {number} */ (node.body.start)] !== '(') {
+ state.str.appendLeft(end, `)`);
+ }
- // in a case like `$: ({ a } = b())`, there's already a trailing parenthesis.
- // otherwise, we need to add one
- if (state.str.original[/** @type {number} */ (node.body.start)] !== '(') {
- state.str.appendLeft(end, `)`);
+ return;
}
- return;
- } else {
- for (const binding of reassigned_bindings) {
- if (binding && (ids.includes(binding.node) || expression_ids.length === 0)) {
+ for (const binding of bindings) {
+ if (binding.reassigned && (ids.includes(binding.node) || expression_ids.length === 0)) {
check_rune_binding('state');
const init =
binding.kind === 'state'
? ' = $state()'
: expression_ids.length === 0
- ? ` = $state(${state.str.original.substring(/** @type {number} */ (node.body.expression.right.start), node.body.expression.right.end)})`
+ ? ` = $state(${state.str.original.substring(/** @type {number} */ (right.start), right.end)})`
: '';
// implicitly-declared variable which we need to make explicit
state.str.prependLeft(
@@ -1000,7 +999,8 @@ const instance_script = {
);
}
}
- if (expression_ids.length === 0 && !bindings.some((b) => b?.kind === 'store_sub')) {
+
+ if (expression_ids.length === 0 && bindings.every((b) => b.kind !== 'store_sub')) {
state.str.remove(/** @type {number} */ (node.start), /** @type {number} */ (node.end));
return;
}
@@ -1592,7 +1592,6 @@ function extract_type_and_comment(declarator, state, path) {
const comment_start = /** @type {any} */ (comment_node)?.start;
const comment_end = /** @type {any} */ (comment_node)?.end;
let comment = comment_node && str.original.substring(comment_start, comment_end);
-
if (comment_node) {
str.update(comment_start, comment_end, '');
}
@@ -1673,6 +1672,11 @@ function extract_type_and_comment(declarator, state, path) {
state.has_type_or_fallback = true;
const match = /@type {(.+)}/.exec(comment_node.value);
if (match) {
+ // try to find JSDoc comments after a hyphen `-`
+ const jsdoc_comment = /@type {.+} (?:\w+|\[.*?\]) - (.+)/.exec(comment_node.value);
+ if (jsdoc_comment) {
+ cleaned_comment += jsdoc_comment[1]?.trim();
+ }
return {
type: match[1],
comment: cleaned_comment,
@@ -1693,7 +1697,6 @@ function extract_type_and_comment(declarator, state, path) {
};
}
}
-
return {
type: 'any',
comment: state.uses_ts ? comment : cleaned_comment,
diff --git a/packages/svelte/src/compiler/phases/1-parse/acorn.js b/packages/svelte/src/compiler/phases/1-parse/acorn.js
index 6c106e0a84..36f7688c49 100644
--- a/packages/svelte/src/compiler/phases/1-parse/acorn.js
+++ b/packages/svelte/src/compiler/phases/1-parse/acorn.js
@@ -1,11 +1,9 @@
/** @import { Comment, Program } from 'estree' */
-/** @import { Node } from 'acorn' */
import * as acorn from 'acorn';
import { walk } from 'zimmerframe';
-import { tsPlugin } from 'acorn-typescript';
-import { locator } from '../../state.js';
+import { tsPlugin } from '@sveltejs/acorn-typescript';
-const ParserWithTS = acorn.Parser.extend(tsPlugin({ allowSatisfies: true }));
+const ParserWithTS = acorn.Parser.extend(tsPlugin());
/**
* @param {string} source
@@ -48,7 +46,6 @@ export function parse(source, typescript, is_script) {
}
}
- if (typescript) amend(source, ast);
add_comments(ast);
return /** @type {Program} */ (ast);
@@ -71,7 +68,6 @@ export function parse_expression_at(source, typescript, index) {
locations: true
});
- if (typescript) amend(source, ast);
add_comments(ast);
return ast;
@@ -173,42 +169,3 @@ function get_comment_handlers(source) {
}
};
}
-
-/**
- * Tidy up some stuff left behind by acorn-typescript
- * @param {string} source
- * @param {Node} node
- */
-function amend(source, node) {
- return walk(node, null, {
- _(node, context) {
- // @ts-expect-error
- delete node.loc.start.index;
- // @ts-expect-error
- delete node.loc.end.index;
-
- if (typeof node.loc?.end === 'number') {
- const loc = locator(node.loc.end);
- if (loc) {
- node.loc.end = {
- line: loc.line,
- column: loc.column
- };
- }
- }
-
- if (
- /** @type {any} */ (node).typeAnnotation &&
- (node.end === undefined || node.end < node.start)
- ) {
- // i think there might be a bug in acorn-typescript that prevents
- // `end` from being assigned when there's a type annotation
- let end = /** @type {any} */ (node).typeAnnotation.start;
- while (/\s/.test(source[end - 1])) end -= 1;
- node.end = end;
- }
-
- context.next();
- }
- });
-}
diff --git a/packages/svelte/src/compiler/phases/1-parse/ambient.d.ts b/packages/svelte/src/compiler/phases/1-parse/ambient.d.ts
deleted file mode 100644
index 8243dd0a42..0000000000
--- a/packages/svelte/src/compiler/phases/1-parse/ambient.d.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-// Silence the acorn typescript errors through this ambient type definition + tsconfig.json path alias
-// That way we can omit `"skipLibCheck": true` and catch other errors in our d.ts files
-declare module 'acorn-typescript';
diff --git a/packages/svelte/src/compiler/phases/1-parse/read/expression.js b/packages/svelte/src/compiler/phases/1-parse/read/expression.js
index 82a667d38c..a596cdf572 100644
--- a/packages/svelte/src/compiler/phases/1-parse/read/expression.js
+++ b/packages/svelte/src/compiler/phases/1-parse/read/expression.js
@@ -38,6 +38,10 @@ export default function read_expression(parser, opening_token, disallow_loose) {
let num_parens = 0;
+ if (node.leadingComments !== undefined && node.leadingComments.length > 0) {
+ parser.index = node.leadingComments.at(-1).end;
+ }
+
for (let i = parser.index; i < /** @type {number} */ (node.start); i += 1) {
if (parser.template[i] === '(') num_parens += 1;
}
diff --git a/packages/svelte/src/compiler/phases/1-parse/read/style.js b/packages/svelte/src/compiler/phases/1-parse/read/style.js
index 29e8a0e541..56dbe124b7 100644
--- a/packages/svelte/src/compiler/phases/1-parse/read/style.js
+++ b/packages/svelte/src/compiler/phases/1-parse/read/style.js
@@ -118,6 +118,7 @@ function read_rule(parser) {
metadata: {
parent_rule: null,
has_local_selectors: false,
+ has_global_selectors: false,
is_global_block: false
}
};
@@ -342,6 +343,7 @@ function read_selector(parser, inside_pseudo_class = false) {
children,
metadata: {
rule: null,
+ is_global: false,
used: false
}
};
@@ -566,7 +568,7 @@ function read_attribute_value(parser) {
}
/**
- * https://www.w3.org/TR/CSS21/syndata.html#value-def-identifier
+ * https://www.w3.org/TR/css-syntax-3/#ident-token-diagram
* @param {Parser} parser
*/
function read_identifier(parser) {
@@ -574,7 +576,7 @@ function read_identifier(parser) {
let identifier = '';
- if (parser.match('--') || parser.match_regex(REGEX_LEADING_HYPHEN_OR_DIGIT)) {
+ if (parser.match_regex(REGEX_LEADING_HYPHEN_OR_DIGIT)) {
e.css_expected_identifier(start);
}
diff --git a/packages/svelte/src/compiler/phases/1-parse/remove_typescript_nodes.js b/packages/svelte/src/compiler/phases/1-parse/remove_typescript_nodes.js
index 18c805128d..aba94ee20d 100644
--- a/packages/svelte/src/compiler/phases/1-parse/remove_typescript_nodes.js
+++ b/packages/svelte/src/compiler/phases/1-parse/remove_typescript_nodes.js
@@ -1,7 +1,7 @@
/** @import { Context, Visitors } from 'zimmerframe' */
/** @import { FunctionExpression, FunctionDeclaration } from 'estree' */
import { walk } from 'zimmerframe';
-import * as b from '../../utils/builders.js';
+import * as b from '#compiler/builders';
import * as e from '../../errors.js';
/**
@@ -24,6 +24,7 @@ const visitors = {
// until that day comes, we just delete them so they don't confuse esrap
delete n.typeAnnotation;
delete n.typeParameters;
+ delete n.typeArguments;
delete n.returnType;
delete n.accessibility;
},
@@ -94,6 +95,9 @@ const visitors = {
TSTypeAliasDeclaration() {
return b.empty;
},
+ TSTypeAssertion(node, context) {
+ return context.visit(node.expression);
+ },
TSEnumDeclaration(node) {
e.typescript_invalid_feature(node, 'enums');
},
@@ -118,6 +122,12 @@ const visitors = {
delete node.implements;
return context.next();
},
+ MethodDefinition(node, context) {
+ if (node.abstract) {
+ return b.empty;
+ }
+ return context.next();
+ },
VariableDeclaration(node, context) {
if (node.declare) {
return b.empty;
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/1-parse/utils/fuzzymatch.js b/packages/svelte/src/compiler/phases/1-parse/utils/fuzzymatch.js
index cd72d73005..28b314cdd5 100644
--- a/packages/svelte/src/compiler/phases/1-parse/utils/fuzzymatch.js
+++ b/packages/svelte/src/compiler/phases/1-parse/utils/fuzzymatch.js
@@ -12,8 +12,8 @@ export default function fuzzymatch(name, names) {
return matches && matches[0][0] > 0.7 ? matches[0][1] : null;
}
-// adapted from https://github.com/Glench/fuzzyset.js/blob/master/lib/fuzzyset.js
-// BSD Licensed
+// adapted from https://github.com/Glench/fuzzyset.js/blob/master/lib/fuzzyset.js in 2016
+// BSD Licensed (see https://github.com/Glench/fuzzyset.js/issues/10)
const GRAM_SIZE_LOWER = 2;
const GRAM_SIZE_UPPER = 3;
diff --git a/packages/svelte/src/compiler/phases/2-analyze/css/css-analyze.js b/packages/svelte/src/compiler/phases/2-analyze/css/css-analyze.js
index ed22838582..2dc3435648 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/css/css-analyze.js
+++ b/packages/svelte/src/compiler/phases/2-analyze/css/css-analyze.js
@@ -7,13 +7,15 @@ import { is_keyframes_node } from '../../css.js';
import { is_global, is_unscoped_pseudo_class } from './utils.js';
/**
- * @typedef {Visitors<
- * AST.CSS.Node,
- * {
- * keyframes: string[];
- * rule: AST.CSS.Rule | null;
- * }
- * >} CssVisitors
+ * @typedef {{
+ * keyframes: string[];
+ * rule: AST.CSS.Rule | null;
+ * analysis: ComponentAnalysis;
+ * }} CssState
+ */
+
+/**
+ * @typedef {Visitors} CssVisitors
*/
/**
@@ -28,6 +30,15 @@ function is_global_block_selector(simple_selector) {
);
}
+/**
+ * @param {AST.SvelteNode[]} path
+ */
+function is_unscoped(path) {
+ return path
+ .filter((node) => node.type === 'Rule')
+ .every((node) => node.metadata.has_global_selectors);
+}
+
/**
*
* @param {Array} path
@@ -42,6 +53,9 @@ const css_visitors = {
if (is_keyframes_node(node)) {
if (!node.prelude.startsWith('-global-') && !is_in_global_block(context.path)) {
context.state.keyframes.push(node.prelude);
+ } else if (node.prelude.startsWith('-global-')) {
+ // we don't check if the block.children.length because the keyframe is still added even if empty
+ context.state.analysis.css.has_global ||= is_unscoped(context.path);
}
}
@@ -99,10 +113,12 @@ const css_visitors = {
node.metadata.rule = context.state.rule;
- node.metadata.used ||= node.children.every(
+ node.metadata.is_global = node.children.every(
({ metadata }) => metadata.is_global || metadata.is_global_like
);
+ node.metadata.used ||= node.metadata.is_global;
+
if (
node.metadata.rule?.metadata.parent_rule &&
node.children[0]?.selectors[0]?.type === 'NestingSelector'
@@ -133,7 +149,13 @@ const css_visitors = {
node.metadata.is_global = node.selectors.length >= 1 && is_global(node);
- if (node.selectors.length === 1) {
+ if (
+ node.selectors.length >= 1 &&
+ node.selectors.every(
+ (selector) =>
+ selector.type === 'PseudoClassSelector' || selector.type === 'PseudoElementSelector'
+ )
+ ) {
const first = node.selectors[0];
node.metadata.is_global_like ||=
(first.type === 'PseudoClassSelector' && first.name === 'host') ||
@@ -171,10 +193,12 @@ const css_visitors = {
Rule(node, context) {
node.metadata.parent_rule = context.state.rule;
- node.metadata.is_global_block = node.prelude.children.some((selector) => {
+ // We gotta allow :global x, :global y because CSS preprocessors might generate that from :global { x, y {...} }
+ for (const complex_selector of node.prelude.children) {
let is_global_block = false;
- for (const child of selector.children) {
+ for (let selector_idx = 0; selector_idx < complex_selector.children.length; selector_idx++) {
+ const child = complex_selector.children[selector_idx];
const idx = child.selectors.findIndex(is_global_block_selector);
if (is_global_block) {
@@ -182,70 +206,79 @@ const css_visitors = {
child.metadata.is_global_like = true;
}
- if (idx !== -1) {
- is_global_block = true;
- for (let i = idx + 1; i < child.selectors.length; i++) {
- walk(/** @type {AST.CSS.Node} */ (child.selectors[i]), null, {
- ComplexSelector(node) {
- node.metadata.used = true;
- }
- });
- }
- }
- }
+ if (idx === 0) {
+ if (
+ child.selectors.length > 1 &&
+ selector_idx === 0 &&
+ node.metadata.parent_rule === null
+ ) {
+ e.css_global_block_invalid_modifier_start(child.selectors[1]);
+ } else {
+ // `child` starts with `:global`
+ node.metadata.is_global_block = is_global_block = true;
+
+ for (let i = 1; i < child.selectors.length; i++) {
+ walk(/** @type {AST.CSS.Node} */ (child.selectors[i]), null, {
+ ComplexSelector(node) {
+ node.metadata.used = true;
+ }
+ });
+ }
- return is_global_block;
- });
+ if (child.combinator && child.combinator.name !== ' ') {
+ e.css_global_block_invalid_combinator(child, child.combinator.name);
+ }
- if (node.metadata.is_global_block) {
- if (node.prelude.children.length > 1) {
- e.css_global_block_invalid_list(node.prelude);
- }
+ const declaration = node.block.children.find((child) => child.type === 'Declaration');
+ const is_lone_global =
+ complex_selector.children.length === 1 &&
+ complex_selector.children[0].selectors.length === 1; // just `:global`, not e.g. `:global x`
- const complex_selector = node.prelude.children[0];
- const global_selector = complex_selector.children.find((r, selector_idx) => {
- const idx = r.selectors.findIndex(is_global_block_selector);
- if (idx === 0) {
- if (r.selectors.length > 1 && selector_idx === 0 && node.metadata.parent_rule === null) {
- e.css_global_block_invalid_modifier_start(r.selectors[1]);
+ if (is_lone_global && node.prelude.children.length > 1) {
+ // `:global, :global x { z { ... } }` would become `x { z { ... } }` which means `z` is always
+ // constrained by `x`, which is not what the user intended
+ e.css_global_block_invalid_list(node.prelude);
+ }
+
+ if (
+ declaration &&
+ // :global { color: red; } is invalid, but foo :global { color: red; } is valid
+ node.prelude.children.length === 1 &&
+ is_lone_global
+ ) {
+ e.css_global_block_invalid_declaration(declaration);
+ }
}
- return true;
} else if (idx !== -1) {
- e.css_global_block_invalid_modifier(r.selectors[idx]);
+ e.css_global_block_invalid_modifier(child.selectors[idx]);
}
- });
-
- if (!global_selector) {
- throw new Error('Internal error: global block without :global selector');
}
- if (global_selector.combinator && global_selector.combinator.name !== ' ') {
- e.css_global_block_invalid_combinator(global_selector, global_selector.combinator.name);
+ if (node.metadata.is_global_block && !is_global_block) {
+ e.css_global_block_invalid_list(node.prelude);
}
+ }
- const declaration = node.block.children.find((child) => child.type === 'Declaration');
+ const state = { ...context.state, rule: node };
- if (
- declaration &&
- // :global { color: red; } is invalid, but foo :global { color: red; } is valid
- node.prelude.children.length === 1 &&
- node.prelude.children[0].children.length === 1 &&
- node.prelude.children[0].children[0].selectors.length === 1
- ) {
- e.css_global_block_invalid_declaration(declaration);
- }
+ // visit selector list first, to populate child selector metadata
+ context.visit(node.prelude, state);
+
+ for (const selector of node.prelude.children) {
+ node.metadata.has_global_selectors ||= selector.metadata.is_global;
+ node.metadata.has_local_selectors ||= !selector.metadata.is_global;
}
- context.next({
- ...context.state,
- rule: node
- });
+ // if this rule has a ComplexSelector whose RelativeSelector children are all
+ // `:global(...)`, and the rule contains declarations (rather than just
+ // nested rules) then the component as a whole includes global CSS
+ context.state.analysis.css.has_global ||=
+ node.metadata.has_global_selectors &&
+ node.block.children.filter((child) => child.type === 'Declaration').length > 0 &&
+ is_unscoped(context.path);
- node.metadata.has_local_selectors = node.prelude.children.some((selector) => {
- return selector.children.some(
- ({ metadata }) => !metadata.is_global && !metadata.is_global_like
- );
- });
+ // visit block list, so parent rule metadata is populated
+ context.visit(node.block, state);
},
NestingSelector(node, context) {
const rule = /** @type {AST.CSS.Rule} */ (context.state.rule);
@@ -283,5 +316,12 @@ const css_visitors = {
* @param {ComponentAnalysis} analysis
*/
export function analyze_css(stylesheet, analysis) {
- walk(stylesheet, { keyframes: analysis.css.keyframes, rule: null }, css_visitors);
+ /** @type {CssState} */
+ const css_state = {
+ keyframes: analysis.css.keyframes,
+ rule: null,
+ analysis
+ };
+
+ walk(stylesheet, css_state, css_visitors);
}
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..fbe6ca1cd3 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
@@ -1,13 +1,21 @@
/** @import * as Compiler from '#compiler' */
import { walk } from 'zimmerframe';
-import { get_parent_rules, get_possible_values, is_outer_global } from './utils.js';
+import {
+ get_parent_rules,
+ get_possible_values,
+ is_outer_global,
+ is_unscoped_pseudo_class
+} from './utils.js';
import { regex_ends_with_whitespace, regex_starts_with_whitespace } from '../../patterns.js';
import { get_attribute_chunks, is_text_attribute } from '../../../utils/ast.js';
/** @typedef {NODE_PROBABLY_EXISTS | NODE_DEFINITELY_EXISTS} NodeExistsValue */
+/** @typedef {FORWARD | BACKWARD} Direction */
const NODE_PROBABLY_EXISTS = 0;
const NODE_DEFINITELY_EXISTS = 1;
+const FORWARD = 0;
+const BACKWARD = 1;
const whitelist_attribute_selector = new Map([
['details', ['open']],
@@ -43,6 +51,27 @@ const nesting_selector = {
}
};
+/** @type {Compiler.AST.CSS.RelativeSelector} */
+const any_selector = {
+ type: 'RelativeSelector',
+ start: -1,
+ end: -1,
+ combinator: null,
+ selectors: [
+ {
+ type: 'TypeSelector',
+ name: '*',
+ start: -1,
+ end: -1
+ }
+ ],
+ metadata: {
+ is_global: false,
+ is_global_like: false,
+ scoped: false
+ }
+};
+
/**
* Snippets encountered already (avoids infinite loops)
* @type {Set}
@@ -72,7 +101,8 @@ export function prune(stylesheet, element) {
apply_selector(
selectors,
/** @type {Compiler.AST.CSS.Rule} */ (node.metadata.rule),
- element
+ element,
+ BACKWARD
)
) {
node.metadata.used = true;
@@ -159,16 +189,17 @@ function truncate(node) {
* @param {Compiler.AST.CSS.RelativeSelector[]} relative_selectors
* @param {Compiler.AST.CSS.Rule} rule
* @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement} element
+ * @param {Direction} direction
* @returns {boolean}
*/
-function apply_selector(relative_selectors, rule, element) {
- const parent_selectors = relative_selectors.slice();
- const relative_selector = parent_selectors.pop();
+function apply_selector(relative_selectors, rule, element, direction) {
+ const rest_selectors = relative_selectors.slice();
+ const relative_selector = direction === FORWARD ? rest_selectors.shift() : rest_selectors.pop();
const matched =
!!relative_selector &&
- relative_selector_might_apply_to_node(relative_selector, rule, element) &&
- apply_combinator(relative_selector, parent_selectors, rule, element);
+ relative_selector_might_apply_to_node(relative_selector, rule, element, direction) &&
+ apply_combinator(relative_selector, rest_selectors, rule, element, direction);
if (matched) {
if (!is_outer_global(relative_selector)) {
@@ -183,76 +214,67 @@ function apply_selector(relative_selectors, rule, element) {
/**
* @param {Compiler.AST.CSS.RelativeSelector} relative_selector
- * @param {Compiler.AST.CSS.RelativeSelector[]} parent_selectors
+ * @param {Compiler.AST.CSS.RelativeSelector[]} rest_selectors
* @param {Compiler.AST.CSS.Rule} rule
* @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement | Compiler.AST.RenderTag | Compiler.AST.Component | Compiler.AST.SvelteComponent | Compiler.AST.SvelteSelf} node
+ * @param {Direction} direction
* @returns {boolean}
*/
-function apply_combinator(relative_selector, parent_selectors, rule, node) {
- if (!relative_selector.combinator) return true;
-
- const name = relative_selector.combinator.name;
+function apply_combinator(relative_selector, rest_selectors, rule, node, direction) {
+ const combinator =
+ direction == FORWARD ? rest_selectors[0]?.combinator : relative_selector.combinator;
+ if (!combinator) return true;
- switch (name) {
+ switch (combinator.name) {
case ' ':
case '>': {
+ const is_adjacent = combinator.name === '>';
+ const parents =
+ direction === FORWARD
+ ? get_descendant_elements(node, is_adjacent)
+ : get_ancestor_elements(node, is_adjacent);
let parent_matched = false;
- const path = node.metadata.path;
- let i = path.length;
-
- while (i--) {
- const parent = path[i];
-
- if (parent.type === 'SnippetBlock') {
- if (seen.has(parent)) {
- parent_matched = true;
- } else {
- seen.add(parent);
-
- for (const site of parent.metadata.sites) {
- if (apply_combinator(relative_selector, parent_selectors, rule, site)) {
- parent_matched = true;
- }
- }
- }
-
- break;
- }
-
- if (parent.type === 'RegularElement' || parent.type === 'SvelteElement') {
- if (apply_selector(parent_selectors, rule, parent)) {
- parent_matched = true;
- }
-
- if (name === '>') return parent_matched;
+ for (const parent of parents) {
+ if (apply_selector(rest_selectors, rule, parent, direction)) {
+ parent_matched = true;
}
}
- return parent_matched || parent_selectors.every((selector) => is_global(selector, rule));
+ return (
+ parent_matched ||
+ (direction === BACKWARD &&
+ (!is_adjacent || parents.length === 0) &&
+ rest_selectors.every((selector) => is_global(selector, rule)))
+ );
}
case '+':
case '~': {
- const siblings = get_possible_element_siblings(node, name === '+');
+ const siblings = get_possible_element_siblings(node, direction, combinator.name === '+');
let sibling_matched = false;
for (const possible_sibling of siblings.keys()) {
- if (possible_sibling.type === 'RenderTag' || possible_sibling.type === 'SlotElement') {
+ if (
+ possible_sibling.type === 'RenderTag' ||
+ possible_sibling.type === 'SlotElement' ||
+ possible_sibling.type === 'Component'
+ ) {
// `{@render foo()}foo
` with `:global(.x) + p` is a match
- if (parent_selectors.length === 1 && parent_selectors[0].metadata.is_global) {
+ if (rest_selectors.length === 1 && rest_selectors[0].metadata.is_global) {
sibling_matched = true;
}
- } else if (apply_selector(parent_selectors, rule, possible_sibling)) {
+ } else if (apply_selector(rest_selectors, rule, possible_sibling, direction)) {
sibling_matched = true;
}
}
return (
sibling_matched ||
- (get_element_parent(node) === null &&
- parent_selectors.every((selector) => is_global(selector, rule)))
+ (direction === BACKWARD &&
+ get_element_parent(node) === null &&
+ rest_selectors.every((selector) => is_global(selector, rule)))
);
}
@@ -269,20 +291,26 @@ function apply_combinator(relative_selector, parent_selectors, rule, node) {
* a global selector
* @param {Compiler.AST.CSS.RelativeSelector} selector
* @param {Compiler.AST.CSS.Rule} rule
+ * @returns {boolean}
*/
function is_global(selector, rule) {
if (selector.metadata.is_global || selector.metadata.is_global_like) {
return true;
}
+ let explicitly_global = false;
+
for (const s of selector.selectors) {
/** @type {Compiler.AST.CSS.SelectorList | null} */
let selector_list = null;
+ let can_be_global = false;
let owner = rule;
if (s.type === 'PseudoClassSelector') {
if ((s.name === 'is' || s.name === 'where') && s.args) {
selector_list = s.args;
+ } else {
+ can_be_global = is_unscoped_pseudo_class(s);
}
}
@@ -291,18 +319,19 @@ function is_global(selector, rule) {
selector_list = owner.prelude;
}
- const has_global_selectors = selector_list?.children.some((complex_selector) => {
+ const has_global_selectors = !!selector_list?.children.some((complex_selector) => {
return complex_selector.children.every((relative_selector) =>
is_global(relative_selector, owner)
);
});
+ explicitly_global ||= has_global_selectors;
- if (!has_global_selectors) {
+ if (!has_global_selectors && !can_be_global) {
return false;
}
}
- return true;
+ return explicitly_global || selector.selectors.length === 0;
}
const regex_backslash_and_following_character = /\\(.)/g;
@@ -313,9 +342,10 @@ const regex_backslash_and_following_character = /\\(.)/g;
* @param {Compiler.AST.CSS.RelativeSelector} relative_selector
* @param {Compiler.AST.CSS.Rule} rule
* @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement} element
+ * @param {Direction} direction
* @returns {boolean}
*/
-function relative_selector_might_apply_to_node(relative_selector, rule, element) {
+function relative_selector_might_apply_to_node(relative_selector, rule, element, direction) {
// Sort :has(...) selectors in one bucket and everything else into another
const has_selectors = [];
const other_selectors = [];
@@ -331,63 +361,21 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element)
// If we're called recursively from a :has(...) selector, we're on the way of checking if the other selectors match.
// In that case ignore this check (because we just came from this) to avoid an infinite loop.
if (has_selectors.length > 0) {
- /** @type {Array} */
- const child_elements = [];
- /** @type {Array} */
- const descendant_elements = [];
- /** @type {Array} */
- 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) {
- child_elements.push(element);
- descendant_elements.push(element);
- }
-
- const seen = new Set();
-
- /**
- * @param {Compiler.AST.SvelteNode} node
- * @param {{ is_child: boolean }} state
- */
- function walk_children(node, state) {
- walk(node, state, {
- _(node, context) {
- if (node.type === 'RegularElement' || node.type === 'SvelteElement') {
- descendant_elements.push(node);
-
- if (context.state.is_child) {
- child_elements.push(node);
- context.state.is_child = false;
- context.next();
- context.state.is_child = true;
- } else {
- context.next();
- }
- } else if (node.type === 'RenderTag') {
- for (const snippet of node.metadata.snippets) {
- if (seen.has(snippet)) continue;
-
- seen.add(snippet);
- walk_children(snippet.body, context.state);
- }
- } else {
- context.next();
- }
- }
- });
- }
-
- walk_children(element.fragment, { is_child: true });
// :has(...) is special in that it means "look downwards in the CSS tree". Since our matching algorithm goes
// upwards and back-to-front, we need to first check the selectors inside :has(...), then check the rest of the
@@ -398,37 +386,34 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element)
let matched = false;
for (const complex_selector of complex_selectors) {
- const selectors = truncate(complex_selector);
- const left_most_combinator = selectors[0]?.combinator ?? descendant_combinator;
- // In .x:has(> y), we want to search for y, ignoring the left-most combinator
- // (else it would try to walk further up and fail because there are no selectors left)
- if (selectors.length > 0) {
- selectors[0] = {
- ...selectors[0],
- combinator: null
- };
+ const [first, ...rest] = truncate(complex_selector);
+ // if it was just a :global(...)
+ if (!first) {
+ complex_selector.metadata.used = true;
+ matched = true;
+ continue;
}
- const descendants =
- left_most_combinator.name === '+' || left_most_combinator.name === '~'
- ? (sibling_elements ??= get_following_sibling_elements(element, include_self))
- : left_most_combinator.name === '>'
- ? child_elements
- : descendant_elements;
-
- let selector_matched = false;
-
- // Iterate over all descendant elements and check if the selector inside :has matches
- for (const element of descendants) {
- if (
- selectors.length === 0 /* is :global(...) */ ||
- (element.metadata.scoped && selector_matched) ||
- apply_selector(selectors, rule, element)
- ) {
+ if (include_self) {
+ const selector_including_self = [
+ first.combinator ? { ...first, combinator: null } : first,
+ ...rest
+ ];
+ if (apply_selector(selector_including_self, rule, element, FORWARD)) {
complex_selector.metadata.used = true;
- selector_matched = matched = true;
+ matched = true;
}
}
+
+ const selector_excluding_self = [
+ any_selector,
+ first.combinator ? first : { ...first, combinator: descendant_combinator },
+ ...rest
+ ];
+ if (apply_selector(selector_excluding_self, rule, element, FORWARD)) {
+ complex_selector.metadata.used = true;
+ matched = true;
+ }
}
if (!matched) {
@@ -453,7 +438,7 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element)
) {
const args = selector.args;
const complex_selector = args.children[0];
- return apply_selector(complex_selector.children, rule, element);
+ return apply_selector(complex_selector.children, rule, element, BACKWARD);
}
// We came across a :global, everything beyond it is global and therefore a potential match
@@ -502,7 +487,7 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element)
if (is_global) {
complex_selector.metadata.used = true;
matched = true;
- } else if (apply_selector(relative, rule, element)) {
+ } else if (apply_selector(relative, rule, element, BACKWARD)) {
complex_selector.metadata.used = true;
matched = true;
} else if (complex_selector.children.length > 1 && (name == 'is' || name == 'where')) {
@@ -586,7 +571,7 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element)
for (const complex_selector of parent.prelude.children) {
if (
- apply_selector(get_relative_selectors(complex_selector), parent, element) ||
+ apply_selector(get_relative_selectors(complex_selector), parent, element, direction) ||
complex_selector.children.every((s) => is_global(s, parent))
) {
complex_selector.metadata.used = true;
@@ -607,73 +592,6 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element)
return true;
}
-/**
- * @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement} element
- * @param {boolean} include_self
- */
-function get_following_sibling_elements(element, include_self) {
- const path = element.metadata.path;
- let i = path.length;
-
- /** @type {Compiler.AST.SvelteNode} */
- let start = element;
- let nodes = /** @type {Compiler.AST.SvelteNode[]} */ (
- /** @type {Compiler.AST.Fragment} */ (path[0]).nodes
- );
-
- // find the set of nodes to walk...
- while (i--) {
- const node = path[i];
-
- if (node.type === 'RegularElement' || node.type === 'SvelteElement') {
- nodes = node.fragment.nodes;
- break;
- }
-
- if (node.type !== 'Fragment') {
- start = node;
- }
- }
-
- /** @type {Array} */
- const siblings = [];
-
- // ...then walk them, starting from the node after the one
- // containing the element in question
-
- const seen = new Set();
-
- /** @param {Compiler.AST.SvelteNode} node */
- function get_siblings(node) {
- walk(node, null, {
- RegularElement(node) {
- siblings.push(node);
- },
- SvelteElement(node) {
- siblings.push(node);
- },
- RenderTag(node) {
- for (const snippet of node.metadata.snippets) {
- if (seen.has(snippet)) continue;
-
- seen.add(snippet);
- get_siblings(snippet.body);
- }
- }
- });
- }
-
- for (const node of nodes.slice(nodes.indexOf(start) + 1)) {
- get_siblings(node);
- }
-
- if (include_self) {
- siblings.push(element);
- }
-
- return siblings;
-}
-
/**
* @param {any} operator
* @param {any} expected_value
@@ -810,6 +728,84 @@ function unquote(str) {
return str;
}
+/**
+ * @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement | Compiler.AST.RenderTag | Compiler.AST.Component | Compiler.AST.SvelteComponent | Compiler.AST.SvelteSelf} node
+ * @param {boolean} adjacent_only
+ * @param {Set} seen
+ */
+function get_ancestor_elements(node, adjacent_only, seen = new Set()) {
+ /** @type {Array} */
+ const ancestors = [];
+
+ const path = node.metadata.path;
+ let i = path.length;
+
+ while (i--) {
+ const parent = path[i];
+
+ if (parent.type === 'SnippetBlock') {
+ if (!seen.has(parent)) {
+ seen.add(parent);
+
+ for (const site of parent.metadata.sites) {
+ ancestors.push(...get_ancestor_elements(site, adjacent_only, seen));
+ }
+ }
+
+ break;
+ }
+
+ if (parent.type === 'RegularElement' || parent.type === 'SvelteElement') {
+ ancestors.push(parent);
+ if (adjacent_only) {
+ break;
+ }
+ }
+ }
+
+ return ancestors;
+}
+
+/**
+ * @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement | Compiler.AST.RenderTag | Compiler.AST.Component | Compiler.AST.SvelteComponent | Compiler.AST.SvelteSelf} node
+ * @param {boolean} adjacent_only
+ * @param {Set} seen
+ */
+function get_descendant_elements(node, adjacent_only, seen = new Set()) {
+ /** @type {Array} */
+ const descendants = [];
+
+ /**
+ * @param {Compiler.AST.SvelteNode} node
+ */
+ function walk_children(node) {
+ walk(node, null, {
+ _(node, context) {
+ if (node.type === 'RegularElement' || node.type === 'SvelteElement') {
+ descendants.push(node);
+
+ if (!adjacent_only) {
+ context.next();
+ }
+ } else if (node.type === 'RenderTag') {
+ for (const snippet of node.metadata.snippets) {
+ if (seen.has(snippet)) continue;
+
+ seen.add(snippet);
+ walk_children(snippet.body);
+ }
+ } else {
+ context.next();
+ }
+ }
+ });
+ }
+
+ walk_children(node.type === 'RenderTag' ? node : node.fragment);
+
+ return descendants;
+}
+
/**
* @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement | Compiler.AST.RenderTag | Compiler.AST.Component | Compiler.AST.SvelteComponent | Compiler.AST.SvelteSelf} node
* @returns {Compiler.AST.RegularElement | Compiler.AST.SvelteElement | null}
@@ -831,12 +827,13 @@ function get_element_parent(node) {
/**
* @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement | Compiler.AST.RenderTag | Compiler.AST.Component | Compiler.AST.SvelteComponent | Compiler.AST.SvelteSelf} node
+ * @param {Direction} direction
* @param {boolean} adjacent_only
* @param {Set} seen
- * @returns {Map}
+ * @returns {Map}
*/
-function get_possible_element_siblings(node, adjacent_only, seen = new Set()) {
- /** @type {Map} */
+function get_possible_element_siblings(node, direction, adjacent_only, seen = new Set()) {
+ /** @type {Map} */
const result = new Map();
const path = node.metadata.path;
@@ -847,9 +844,9 @@ function get_possible_element_siblings(node, adjacent_only, seen = new Set()) {
while (i--) {
const fragment = /** @type {Compiler.AST.Fragment} */ (path[i--]);
- let j = fragment.nodes.indexOf(current);
+ let j = fragment.nodes.indexOf(current) + (direction === FORWARD ? 1 : -1);
- while (j--) {
+ while (j >= 0 && j < fragment.nodes.length) {
const node = fragment.nodes[j];
if (node.type === 'RegularElement') {
@@ -864,21 +861,32 @@ function get_possible_element_siblings(node, adjacent_only, seen = new Set()) {
return result;
}
}
- } else if (is_block(node)) {
- if (node.type === 'SlotElement') {
+ // Special case: slots, render tags and svelte:element tags could resolve to no siblings,
+ // so we want to continue until we find a definite sibling even with the adjacent-only combinator
+ } else if (is_block(node) || node.type === 'Component') {
+ if (node.type === 'SlotElement' || node.type === 'Component') {
result.set(node, NODE_PROBABLY_EXISTS);
}
- const possible_last_child = get_possible_last_child(node, adjacent_only);
+ const possible_last_child = get_possible_nested_siblings(node, direction, adjacent_only);
add_to_map(possible_last_child, result);
- if (adjacent_only && has_definite_elements(possible_last_child)) {
+ if (
+ adjacent_only &&
+ node.type !== 'Component' &&
+ has_definite_elements(possible_last_child)
+ ) {
return result;
}
- } else if (node.type === 'RenderTag' || node.type === 'SvelteElement') {
+ } else if (node.type === 'SvelteElement') {
result.set(node, NODE_PROBABLY_EXISTS);
- // Special case: slots, render tags and svelte:element tags could resolve to no siblings,
- // so we want to continue until we find a definite sibling even with the adjacent-only combinator
+ } else if (node.type === 'RenderTag') {
+ result.set(node, NODE_PROBABLY_EXISTS);
+ for (const snippet of node.metadata.snippets) {
+ add_to_map(get_possible_nested_siblings(snippet, direction, adjacent_only), result);
+ }
}
+
+ j = direction === FORWARD ? j + 1 : j - 1;
}
current = path[i];
@@ -898,7 +906,7 @@ function get_possible_element_siblings(node, adjacent_only, seen = new Set()) {
seen.add(current);
for (const site of current.metadata.sites) {
- const siblings = get_possible_element_siblings(site, adjacent_only, seen);
+ const siblings = get_possible_element_siblings(site, direction, adjacent_only, seen);
add_to_map(siblings, result);
if (adjacent_only && current.metadata.sites.size === 1 && has_definite_elements(siblings)) {
@@ -911,7 +919,7 @@ function get_possible_element_siblings(node, adjacent_only, seen = new Set()) {
if (current.type === 'EachBlock' && fragment === current.body) {
// `{#each ...} {/each}` — `` can be previous sibling of ` `
- add_to_map(get_possible_last_child(current, adjacent_only), result);
+ add_to_map(get_possible_nested_siblings(current, direction, adjacent_only), result);
}
}
@@ -919,13 +927,13 @@ 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 {Compiler.AST.EachBlock | Compiler.AST.IfBlock | Compiler.AST.AwaitBlock | Compiler.AST.KeyBlock | Compiler.AST.SlotElement | Compiler.AST.SnippetBlock | Compiler.AST.Component} node
+ * @param {Direction} direction
* @param {boolean} adjacent_only
- * @returns {Map}
+ * @param {Set} seen
+ * @returns {Map}
*/
-function get_possible_last_child(node, adjacent_only) {
- /** @typedef {Map} NodeMap */
-
+function get_possible_nested_siblings(node, direction, adjacent_only, seen = new Set()) {
/** @type {Array} */
let fragments = [];
@@ -946,12 +954,24 @@ function get_possible_last_child(node, adjacent_only) {
case 'SlotElement':
fragments.push(node.fragment);
break;
+
+ case 'SnippetBlock':
+ if (seen.has(node)) {
+ return new Map();
+ }
+ seen.add(node);
+ fragments.push(node.body);
+ break;
+
+ case 'Component':
+ fragments.push(node.fragment, ...[...node.metadata.snippets].map((s) => s.body));
+ break;
}
- /** @type {NodeMap} */
+ /** @type {Map} NodeMap */
const result = new Map();
- let exhaustive = node.type !== 'SlotElement';
+ let exhaustive = node.type !== 'SlotElement' && node.type !== 'SnippetBlock';
for (const fragment of fragments) {
if (fragment == null) {
@@ -959,7 +979,7 @@ function get_possible_last_child(node, adjacent_only) {
continue;
}
- const map = loop_child(fragment.nodes, adjacent_only);
+ const map = loop_child(fragment.nodes, direction, adjacent_only, seen);
exhaustive &&= has_definite_elements(map);
add_to_map(map, result);
@@ -989,9 +1009,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) {
@@ -1001,27 +1022,28 @@ function add_to_map(from, to) {
}
/**
- * @param {NodeExistsValue | undefined} exist1
+ * @param {NodeExistsValue} exist1
* @param {NodeExistsValue | undefined} exist2
* @returns {NodeExistsValue}
*/
function higher_existence(exist1, exist2) {
- // @ts-expect-error TODO figure out if this is a bug
- if (exist1 === undefined || exist2 === undefined) return exist1 || exist2;
+ if (exist2 === undefined) return exist1;
return exist1 > exist2 ? exist1 : exist2;
}
/**
* @param {Compiler.AST.SvelteNode[]} children
+ * @param {Direction} direction
* @param {boolean} adjacent_only
+ * @param {Set} seen
*/
-function loop_child(children, adjacent_only) {
- /** @type {Map} */
+function loop_child(children, direction, adjacent_only, seen) {
+ /** @type {Map} */
const result = new Map();
- let i = children.length;
+ let i = direction === FORWARD ? 0 : children.length - 1;
- while (i--) {
+ while (i >= 0 && i < children.length) {
const child = children[i];
if (child.type === 'RegularElement') {
@@ -1029,13 +1051,21 @@ function loop_child(children, adjacent_only) {
if (adjacent_only) {
break;
}
+ } else if (child.type === 'SvelteElement') {
+ result.set(child, NODE_PROBABLY_EXISTS);
+ } else if (child.type === 'RenderTag') {
+ for (const snippet of child.metadata.snippets) {
+ add_to_map(get_possible_nested_siblings(snippet, direction, adjacent_only, seen), result);
+ }
} else if (is_block(child)) {
- const child_result = get_possible_last_child(child, adjacent_only);
+ const child_result = get_possible_nested_siblings(child, direction, adjacent_only, seen);
add_to_map(child_result, result);
if (adjacent_only && has_definite_elements(child_result)) {
break;
}
}
+
+ i = direction === FORWARD ? i + 1 : i - 1;
}
return 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..2e36a89649 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/index.js
+++ b/packages/svelte/src/compiler/phases/2-analyze/index.js
@@ -5,8 +5,8 @@
import { walk } from 'zimmerframe';
import * as e from '../../errors.js';
import * as w from '../../warnings.js';
-import { extract_identifiers, is_text_attribute } from '../../utils/ast.js';
-import * as b from '../../utils/builders.js';
+import { extract_identifiers } from '../../utils/ast.js';
+import * as b from '#compiler/builders';
import { Scope, ScopeRoot, create_scopes, get_rune, set_scope } from '../scope.js';
import check_graph_for_cycles from './utils/check_graph_for_cycles.js';
import { create_attribute, is_custom_element_node } from '../nodes.js';
@@ -243,27 +243,38 @@ 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),
{
scope,
scopes,
- // @ts-expect-error TODO
- analysis
+ analysis: /** @type {ComponentAnalysis} */ (analysis),
+ derived_state: [],
+ // TODO the following are not needed for modules, but we have to pass them in order to avoid type error,
+ // and reducing the type would result in a lot of tedious type casts elsewhere - find a good solution one day
+ ast_type: /** @type {any} */ (null),
+ component_slots: new Set(),
+ expression: null,
+ function_depth: 0,
+ has_props_rune: false,
+ options: /** @type {ValidatedCompileOptions} */ (options),
+ parent_element: null,
+ reactive_statement: null
},
visitors
);
- return {
- module: { ast, scope, scopes },
- name: options.filename,
- accessors: false,
- runes: true,
- immutable: true,
- tracing: analysis.tracing
- };
+ return analysis;
}
/**
@@ -415,11 +426,13 @@ 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,
uses_render_tags: false,
needs_context: false,
+ needs_mutation_validation: false,
needs_props: false,
event_directive_node: null,
uses_event_attributes: false,
@@ -443,7 +456,8 @@ export function analyze_component(root, source, options) {
hash
})
: '',
- keyframes: []
+ keyframes: [],
+ has_global: false
},
source,
undefined_exports: new Map(),
@@ -552,7 +566,7 @@ export function analyze_component(root, source, options) {
binding.declaration_kind !== 'import'
) {
binding.kind = 'state';
- binding.mutated = binding.updated = true;
+ binding.mutated = true;
}
}
}
@@ -604,12 +618,9 @@ 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,
- reactive_statements: new Map()
+ reactive_statement: null
};
walk(/** @type {AST.SvelteNode} */ (ast), state, visitors);
@@ -671,13 +682,10 @@ export function analyze_component(root, source, options) {
parent_element: null,
has_props_rune: false,
ast_type: ast === instance.ast ? 'instance' : ast === template.ast ? 'template' : 'module',
- instance_scope: instance.scope,
reactive_statement: null,
- reactive_statements: analysis.reactive_statements,
component_slots: new Set(),
expression: null,
- render_tag: null,
- private_derived_state: [],
+ derived_state: [],
function_depth: scope.function_depth
};
@@ -755,66 +763,62 @@ export function analyze_component(root, source, options) {
if (!should_ignore_unused) {
warn_unused(analysis.css.ast);
}
+ }
- outer: for (const node of analysis.elements) {
- if (node.metadata.scoped) {
- // Dynamic elements in dom mode always use spread for attributes and therefore shouldn't have a class attribute added to them
- // TODO this happens during the analysis phase, which shouldn't know anything about client vs server
- if (node.type === 'SvelteElement' && options.generate === 'client') continue;
+ for (const node of analysis.elements) {
+ if (node.metadata.scoped && is_custom_element_node(node)) {
+ mark_subtree_dynamic(node.metadata.path);
+ }
- /** @type {AST.Attribute | undefined} */
- let class_attribute = undefined;
+ let has_class = false;
+ let has_style = false;
+ let has_spread = false;
+ let has_class_directive = false;
+ let has_style_directive = false;
+
+ for (const attribute of node.attributes) {
+ // The spread method appends the hash to the end of the class attribute on its own
+ if (attribute.type === 'SpreadAttribute') {
+ has_spread = true;
+ break;
+ } else if (attribute.type === 'Attribute') {
+ has_class ||= attribute.name.toLowerCase() === 'class';
+ has_style ||= attribute.name.toLowerCase() === 'style';
+ } else if (attribute.type === 'ClassDirective') {
+ has_class_directive = true;
+ } else if (attribute.type === 'StyleDirective') {
+ has_style_directive = true;
+ }
+ }
- for (const attribute of node.attributes) {
- if (attribute.type === 'SpreadAttribute') {
- // The spread method appends the hash to the end of the class attribute on its own
- continue outer;
+ // We need an empty class to generate the set_class() or class="" correctly
+ if (!has_spread && !has_class && (node.metadata.scoped || has_class_directive)) {
+ node.attributes.push(
+ create_attribute('class', -1, -1, [
+ {
+ type: 'Text',
+ data: '',
+ raw: '',
+ start: -1,
+ end: -1
}
+ ])
+ );
+ }
- if (attribute.type !== 'Attribute') continue;
- if (attribute.name.toLowerCase() !== 'class') continue;
- // The dynamic class method appends the hash to the end of the class attribute on its own
- if (attribute.metadata.needs_clsx) continue outer;
-
- class_attribute = attribute;
- }
-
- if (class_attribute && class_attribute.value !== true) {
- if (is_text_attribute(class_attribute)) {
- class_attribute.value[0].data += ` ${analysis.css.hash}`;
- } else {
- /** @type {AST.Text} */
- const css_text = {
- type: 'Text',
- data: ` ${analysis.css.hash}`,
- raw: ` ${analysis.css.hash}`,
- start: -1,
- end: -1
- };
-
- if (Array.isArray(class_attribute.value)) {
- class_attribute.value.push(css_text);
- } else {
- class_attribute.value = [class_attribute.value, css_text];
- }
- }
- } else {
- node.attributes.push(
- create_attribute('class', -1, -1, [
- {
- type: 'Text',
- data: analysis.css.hash,
- raw: analysis.css.hash,
- start: -1,
- end: -1
- }
- ])
- );
- if (is_custom_element_node(node) && node.attributes.length === 1) {
- mark_subtree_dynamic(node.metadata.path);
+ // We need an empty style to generate the set_style() correctly
+ if (!has_spread && !has_style && has_style_directive) {
+ node.attributes.push(
+ create_attribute('style', -1, -1, [
+ {
+ type: 'Text',
+ data: '',
+ raw: '',
+ start: -1,
+ end: -1
}
- }
- }
+ ])
+ );
}
}
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..17c8123de1 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/types.d.ts
+++ b/packages/svelte/src/compiler/phases/2-analyze/types.d.ts
@@ -1,7 +1,6 @@
import type { Scope } from '../scope.js';
import type { ComponentAnalysis, ReactiveStatement } from '../types.js';
import type { AST, ExpressionMetadata, ValidatedCompileOptions } from '#compiler';
-import type { LabeledStatement } from 'estree';
export interface AnalysisState {
scope: Scope;
@@ -19,15 +18,11 @@ 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: { name: string; private: boolean }[];
function_depth: number;
// legacy stuff
- instance_scope: Scope;
reactive_statement: null | ReactiveStatement;
- reactive_statements: Map;
}
export type Context = import('zimmerframe').Context<
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 9d801e095e..3ba81767cc 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)) {
@@ -61,9 +56,6 @@ export function Attribute(node, context) {
) {
continue;
}
-
- node.metadata.expression.has_state ||= chunk.metadata.expression.has_state;
- node.metadata.expression.has_call ||= chunk.metadata.expression.has_call;
}
if (is_event_attribute(node)) {
@@ -170,16 +162,8 @@ function get_delegated_event(event_name, handler, context) {
return unhoisted;
}
- if (binding !== null && binding.initial !== null && !binding.updated && !binding.is_called) {
- const binding_type = binding.initial.type;
-
- if (
- binding_type === 'ArrowFunctionExpression' ||
- binding_type === 'FunctionDeclaration' ||
- binding_type === 'FunctionExpression'
- ) {
- target_function = binding.initial;
- }
+ if (binding?.is_function()) {
+ target_function = binding.initial;
}
}
@@ -199,6 +183,15 @@ function get_delegated_event(event_name, handler, context) {
const binding = scope.get(reference);
const local_binding = context.state.scope.get(reference);
+ // if the function access a snippet that can't be hoisted we bail out
+ if (
+ local_binding !== null &&
+ local_binding.initial?.type === 'SnippetBlock' &&
+ !local_binding.initial.metadata.can_hoist
+ ) {
+ return unhoisted;
+ }
+
// If we are referencing a binding that is shadowed in another scope then bail out.
if (local_binding !== null && binding !== null && local_binding.node !== binding.node) {
return unhoisted;
diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/BindDirective.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/BindDirective.js
index b4de1925df..18ea79262b 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/visitors/BindDirective.js
+++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/BindDirective.js
@@ -5,7 +5,7 @@ import {
is_text_attribute,
object
} from '../../../utils/ast.js';
-import { validate_no_const_assignment } from './shared/utils.js';
+import { validate_assignment } from './shared/utils.js';
import * as e from '../../../errors.js';
import * as w from '../../../warnings.js';
import { binding_properties } from '../../bindings.js';
@@ -132,8 +132,19 @@ export function BindDirective(node, context) {
}
let i = /** @type {number} */ (node.expression.start);
+ let leading_comments_start = /**@type {any}*/ (node.expression.leadingComments?.at(0))?.start;
+ let leading_comments_end = /**@type {any}*/ (node.expression.leadingComments?.at(-1))?.end;
while (context.state.analysis.source[--i] !== '{') {
- if (context.state.analysis.source[i] === '(') {
+ if (
+ context.state.analysis.source[i] === '(' &&
+ // if the parenthesis is in a leading comment we don't need to throw the error
+ !(
+ leading_comments_start &&
+ leading_comments_end &&
+ i <= leading_comments_end &&
+ i >= leading_comments_start
+ )
+ ) {
e.bind_invalid_parens(node, node.name);
}
}
@@ -147,7 +158,7 @@ export function BindDirective(node, context) {
return;
}
- validate_no_const_assignment(node, node.expression, context.state.scope, true);
+ validate_assignment(node, node.expression, context.state);
const assignee = node.expression;
const left = object(assignee);
@@ -173,14 +184,6 @@ export function BindDirective(node, context) {
) {
e.bind_invalid_value(node.expression);
}
-
- if (context.state.analysis.runes && binding?.kind === 'each') {
- e.each_item_invalid_assignment(node);
- }
-
- if (binding?.kind === 'snippet') {
- e.snippet_parameter_assignment(node);
- }
}
if (node.name === 'group') {
@@ -188,6 +191,10 @@ export function BindDirective(node, context) {
throw new Error('Cannot find declaration for bind:group');
}
+ if (binding.kind === 'snippet') {
+ e.bind_group_invalid_snippet_parameter(node);
+ }
+
// Traverse the path upwards and find all EachBlocks who are (indirectly) contributing to bind:group,
// i.e. one of their declarations is referenced in the binding. This allows group bindings to work
// correctly when referencing a variable declared in an EachBlock by using the index of the each block
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 9f51cd61de..904817b014 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js
+++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js
@@ -3,10 +3,10 @@
/** @import { Context } from '../types' */
import { get_rune } from '../../scope.js';
import * as e from '../../../errors.js';
-import { get_parent, unwrap_optional } from '../../../utils/ast.js';
+import { get_parent } from '../../../utils/ast.js';
import { is_pure, is_safe_identifier } from './shared/utils.js';
import { dev, locate_node, source } from '../../../state.js';
-import * as b from '../../../utils/builders.js';
+import * as b from '#compiler/builders';
/**
* @param {CallExpression} node
@@ -17,6 +17,14 @@ export function CallExpression(node, context) {
const rune = get_rune(node, context.state.scope);
+ if (rune && rune !== '$inspect') {
+ for (const arg of node.arguments) {
+ if (arg.type === 'SpreadElement') {
+ e.rune_invalid_spread(node, rune);
+ }
+ }
+ }
+
switch (rune) {
case null:
if (!is_safe_identifier(node.callee, context.state.scope)) {
@@ -42,6 +50,9 @@ export function CallExpression(node, context) {
e.bindable_invalid_location(node);
}
+ // We need context in case the bound prop is stale
+ context.state.analysis.needs_context = true;
+
break;
case '$host':
@@ -55,7 +66,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 +85,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);
@@ -87,7 +125,7 @@ export function CallExpression(node, context) {
if ((rune === '$derived' || rune === '$derived.by') && node.arguments.length !== 1) {
e.rune_invalid_arguments_length(node, rune, 'exactly one argument');
- } else if (rune === '$state' && node.arguments.length > 1) {
+ } else if (node.arguments.length > 1) {
e.rune_invalid_arguments_length(node, rune, 'zero or one arguments');
}
@@ -186,26 +224,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);
-
- if (binding !== null) {
- binding.is_called = true;
- }
- }
-
// `$inspect(foo)` or `$derived(foo) should not trigger the `static-state-reference` warning
if (rune === '$inspect' || rune === '$derived') {
context.next({ ...context.state, function_depth: context.state.function_depth + 1 });
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..0463e4da85 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/visitors/ClassBody.js
+++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/ClassBody.js
@@ -7,21 +7,24 @@ import { get_rune } from '../../scope.js';
* @param {Context} context
*/
export function ClassBody(node, context) {
- /** @type {string[]} */
- const private_derived_state = [];
+ /** @type {{name: string, private: boolean}[]} */
+ 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({
+ name: definition.key.name,
+ private: definition.key.type === 'PrivateIdentifier'
+ });
}
}
}
- context.next({ ...context.state, private_derived_state });
+ context.next({ ...context.state, derived_state });
}
diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/ConstTag.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/ConstTag.js
index 214b4bcb9e..f723f8447c 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/visitors/ConstTag.js
+++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/ConstTag.js
@@ -24,6 +24,8 @@ export function ConstTag(node, context) {
grand_parent?.type !== 'EachBlock' &&
grand_parent?.type !== 'AwaitBlock' &&
grand_parent?.type !== 'SnippetBlock' &&
+ grand_parent?.type !== 'SvelteBoundary' &&
+ grand_parent?.type !== 'KeyBlock' &&
((grand_parent?.type !== 'RegularElement' && grand_parent?.type !== 'SvelteElement') ||
!grand_parent.attributes.some((a) => a.type === 'Attribute' && a.name === 'slot')))
) {
diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/ExportNamedDeclaration.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/ExportNamedDeclaration.js
index 547f6ab9c7..4b85894e52 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/visitors/ExportNamedDeclaration.js
+++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/ExportNamedDeclaration.js
@@ -1,7 +1,5 @@
-/** @import { ExportNamedDeclaration, Identifier, Node } from 'estree' */
-/** @import { Binding } from '#compiler' */
+/** @import { ExportNamedDeclaration, Identifier } from 'estree' */
/** @import { Context } from '../types' */
-/** @import { Scope } from '../../scope' */
import * as e from '../../../errors.js';
import { extract_identifiers } from '../../../utils/ast.js';
diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/ExportSpecifier.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/ExportSpecifier.js
index cfb24970de..2a05ffb926 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/visitors/ExportSpecifier.js
+++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/ExportSpecifier.js
@@ -22,7 +22,7 @@ export function ExportSpecifier(node, context) {
});
const binding = context.state.scope.get(local_name);
- if (binding) binding.reassigned = binding.updated = true;
+ if (binding) binding.reassigned = true;
}
} else {
validate_export(node, context.state.scope, local_name);
diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/Identifier.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/Identifier.js
index 79dccd5a7c..efbbe6cfa2 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/visitors/Identifier.js
+++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/Identifier.js
@@ -1,5 +1,4 @@
/** @import { Expression, Identifier } from 'estree' */
-/** @import { EachBlock } from '#compiler' */
/** @import { Context } from '../types' */
import is_reference from 'is-reference';
import { should_proxy } from '../../3-transform/client/utils.js';
@@ -7,6 +6,7 @@ import * as e from '../../../errors.js';
import * as w from '../../../warnings.js';
import { is_rune } from '../../../../utils.js';
import { mark_subtree_dynamic } from './shared/fragment.js';
+import { get_rune } from '../../scope.js';
/**
* @param {Identifier} node
@@ -111,7 +111,34 @@ export function Identifier(node, context) {
(parent.type !== 'AssignmentExpression' || parent.left !== node) &&
parent.type !== 'UpdateExpression'
) {
- w.state_referenced_locally(node);
+ let type = 'closure';
+
+ let i = context.path.length;
+ while (i--) {
+ const parent = context.path[i];
+
+ if (
+ parent.type === 'ArrowFunctionExpression' ||
+ parent.type === 'FunctionDeclaration' ||
+ parent.type === 'FunctionExpression'
+ ) {
+ break;
+ }
+
+ if (
+ parent.type === 'CallExpression' &&
+ parent.arguments.includes(/** @type {any} */ (context.path[i + 1]))
+ ) {
+ const rune = get_rune(parent, context.state.scope);
+
+ if (rune === '$state' || rune === '$state.raw') {
+ type = 'derived';
+ break;
+ }
+ }
+ }
+
+ w.state_referenced_locally(node, node.name, type);
}
if (
diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/LabeledStatement.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/LabeledStatement.js
index a63480feaa..514cfae53c 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/visitors/LabeledStatement.js
+++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/LabeledStatement.js
@@ -64,7 +64,7 @@ export function LabeledStatement(node, context) {
}
}
- context.state.reactive_statements.set(node, reactive_statement);
+ context.state.analysis.reactive_statements.set(node, reactive_statement);
if (
node.body.type === 'ExpressionStatement' &&
diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/MemberExpression.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/MemberExpression.js
index 6ea8f238e1..245a164c71 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/visitors/MemberExpression.js
+++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/MemberExpression.js
@@ -1,10 +1,7 @@
-/** @import { MemberExpression, Node } from 'estree' */
+/** @import { MemberExpression } from 'estree' */
/** @import { Context } from '../types' */
import * as e from '../../../errors.js';
-import * as w from '../../../warnings.js';
-import { object } from '../../../utils/ast.js';
import { is_pure, is_safe_identifier } from './shared/utils.js';
-import { mark_subtree_dynamic } from './shared/fragment.js';
/**
* @param {MemberExpression} node
diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/RegularElement.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/RegularElement.js
index 03dfaebcb7..d5689e5d55 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/visitors/RegularElement.js
+++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/RegularElement.js
@@ -173,7 +173,8 @@ export function RegularElement(node, context) {
if (
context.state.analysis.source[node.end - 2] === '/' &&
!is_void(node_name) &&
- !is_svg(node_name)
+ !is_svg(node_name) &&
+ !is_mathml(node_name)
) {
w.element_invalid_self_closing_tag(node, node.name);
}
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/TaggedTemplateExpression.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/TaggedTemplateExpression.js
index eacb8a342a..881ee5a85e 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/visitors/TaggedTemplateExpression.js
+++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/TaggedTemplateExpression.js
@@ -1,4 +1,4 @@
-/** @import { TaggedTemplateExpression, VariableDeclarator } from 'estree' */
+/** @import { TaggedTemplateExpression } from 'estree' */
/** @import { Context } from '../types' */
import { is_pure } from './shared/utils.js';
@@ -12,12 +12,5 @@ export function TaggedTemplateExpression(node, context) {
context.state.expression.has_state = true;
}
- if (node.tag.type === 'Identifier') {
- const binding = context.state.scope.get(node.tag.name);
-
- if (binding !== null) {
- binding.is_called = true;
- }
- }
context.next();
}
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..1f58a28cad 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
@@ -30,7 +30,7 @@ const non_interactive_roles = non_abstract_roles
// 'generic' is meant to have no semantic meaning.
// 'cell' is treated as CellRole by the AXObject which is interactive, so we treat 'cell' it as interactive as well.
!['toolbar', 'tabpanel', 'generic', 'cell'].includes(name) &&
- !role?.superClass.some((classes) => classes.includes('widget'))
+ !role?.superClass.some((classes) => classes.includes('widget') || classes.includes('window'))
);
})
.concat(
@@ -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/function.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/function.js
index c6151992bf..c892efd421 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/function.js
+++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/function.js
@@ -15,6 +15,7 @@ export function visit_function(node, context) {
context.next({
...context.state,
- function_depth: context.state.function_depth + 1
+ function_depth: context.state.function_depth + 1,
+ expression: null
});
}
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..12e21c386c 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
@@ -1,4 +1,4 @@
-/** @import { AssignmentExpression, Expression, Literal, Node, Pattern, PrivateIdentifier, Super, UpdateExpression, VariableDeclarator } from 'estree' */
+/** @import { AssignmentExpression, Expression, Literal, Node, Pattern, Super, UpdateExpression, VariableDeclarator } from 'estree' */
/** @import { AST, Binding } from '#compiler' */
/** @import { AnalysisState, Context } from '../../types' */
/** @import { Scope } from '../../../scope' */
@@ -6,23 +6,23 @@
import * as e from '../../../../errors.js';
import { extract_identifiers } from '../../../../utils/ast.js';
import * as w from '../../../../warnings.js';
-import * as b from '../../../../utils/builders.js';
+import * as b from '#compiler/builders';
import { get_rune } from '../../../scope.js';
/**
- * @param {AssignmentExpression | UpdateExpression} node
+ * @param {AssignmentExpression | UpdateExpression | AST.BindDirective} node
* @param {Pattern | Expression} argument
* @param {AnalysisState} state
*/
export function validate_assignment(node, argument, state) {
- validate_no_const_assignment(node, argument, state.scope, false);
+ validate_no_const_assignment(node, argument, state.scope, node.type === 'BindDirective');
if (argument.type === 'Identifier') {
const binding = state.scope.get(argument.name);
if (state.analysis.runes) {
- if (binding?.kind === 'derived') {
- e.constant_assignment(node, 'derived state');
+ if (binding?.node === state.analysis.props_id) {
+ e.constant_assignment(node, '$props.id()');
}
if (binding?.kind === 'each') {
@@ -34,22 +34,6 @@ export function validate_assignment(node, argument, state) {
e.snippet_parameter_assignment(node);
}
}
-
- 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');
- }
- }
}
/**
@@ -74,7 +58,6 @@ export function validate_no_const_assignment(node, argument, scope, is_binding)
} else if (argument.type === 'Identifier') {
const binding = scope.get(argument.name);
if (
- binding?.kind === 'derived' ||
binding?.declaration_kind === 'import' ||
(binding?.declaration_kind === 'const' && binding.kind !== 'each')
) {
@@ -89,12 +72,7 @@ export function validate_no_const_assignment(node, argument, scope, is_binding)
// );
// TODO have a more specific error message for assignments to things like `{:then foo}`
- const thing =
- binding.declaration_kind === 'import'
- ? 'import'
- : binding.kind === 'derived'
- ? 'derived state'
- : 'constant';
+ const thing = binding.declaration_kind === 'import' ? 'import' : 'constant';
if (is_binding) {
e.constant_binding(node, thing);
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 582c32b534..f0da5a4918 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
@@ -3,7 +3,7 @@
/** @import { ComponentAnalysis, Analysis } from '../../types' */
/** @import { Visitors, ComponentClientTransformState, ClientTransformState } from './types' */
import { walk } from 'zimmerframe';
-import * as b from '../../../utils/builders.js';
+import * as b from '#compiler/builders';
import { build_getter, is_state_source } from './utils.js';
import { render_stylesheet } from '../css/index.js';
import { dev, filename } from '../../../state.js';
@@ -169,9 +169,9 @@ export function client_component(analysis, options) {
module_level_snippets: [],
// these are set inside the `Fragment` visitor, and cannot be used until then
- before_init: /** @type {any} */ (null),
init: /** @type {any} */ (null),
update: /** @type {any} */ (null),
+ expressions: /** @type {any} */ (null),
after_update: /** @type {any} */ (null),
template: /** @type {any} */ (null),
locations: /** @type {any} */ (null)
@@ -219,7 +219,10 @@ export function client_component(analysis, options) {
for (const [name, binding] of analysis.instance.scope.declarations) {
if (binding.kind === 'legacy_reactive') {
legacy_reactive_declarations.push(
- b.const(name, b.call('$.mutable_state', undefined, analysis.immutable ? b.true : undefined))
+ b.const(
+ name,
+ b.call('$.mutable_source', undefined, analysis.immutable ? b.true : undefined)
+ )
);
}
if (binding.kind === 'store_sub') {
@@ -312,7 +315,7 @@ export function client_component(analysis, options) {
const setter = b.set(key, [
b.stmt(b.call(b.id(name), b.id('$$value'))),
- b.stmt(b.call('$.flush_sync'))
+ b.stmt(b.call('$.flush'))
]);
if (analysis.runes && binding.initial) {
@@ -390,6 +393,12 @@ export function client_component(analysis, options) {
);
}
+ if (analysis.needs_mutation_validation) {
+ component_block.body.unshift(
+ b.var('$$ownership_validator', b.call('$.create_ownership_validator', b.id('$$props')))
+ );
+ }
+
const should_inject_context =
dev ||
analysis.needs_context ||
@@ -527,9 +536,6 @@ export function client_component(analysis, options) {
b.assignment('=', b.member(b.id(analysis.name), '$.FILENAME', true), b.literal(filename))
)
);
-
- body.unshift(b.stmt(b.call(b.id('$.mark_module_start'))));
- body.push(b.stmt(b.call(b.id('$.mark_module_end'), b.id(analysis.name))));
}
if (!analysis.runes) {
@@ -562,6 +568,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)))))
@@ -591,7 +602,7 @@ export function client_component(analysis, options) {
/** @type {ESTree.Property[]} */ (
[
prop_def.attribute ? b.init('attribute', b.literal(prop_def.attribute)) : undefined,
- prop_def.reflect ? b.init('reflect', b.literal(true)) : undefined,
+ prop_def.reflect ? b.init('reflect', b.true) : undefined,
prop_def.type ? b.init('type', b.literal(prop_def.type)) : undefined
].filter(Boolean)
)
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts b/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts
index 5c8476de3e..243e1c64a3 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts
+++ b/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts
@@ -30,7 +30,7 @@ export interface ClientTransformState extends TransformState {
/** turn `foo` into e.g. `$.get(foo)` */
read: (id: Identifier) => Expression;
/** turn `foo = bar` into e.g. `$.set(foo, bar)` */
- assign?: (node: Identifier, value: Expression) => Expression;
+ assign?: (node: Identifier, value: Expression, proxy?: boolean) => Expression;
/** turn `foo.bar = baz` into e.g. `$.mutate(foo, $.get(foo).bar = baz);` */
mutate?: (node: Identifier, mutation: AssignmentExpression | UpdateExpression) => Expression;
/** turn `foo++` into e.g. `$.update(foo)` */
@@ -45,15 +45,16 @@ export interface ComponentClientTransformState extends ClientTransformState {
readonly hoisted: Array;
readonly events: Set;
readonly is_instance: boolean;
+ readonly store_to_invalidate?: string;
- /** Stuff that happens before the render effect(s) */
- readonly before_init: Statement[];
/** Stuff that happens before the render effect(s) */
readonly init: Statement[];
/** Stuff that happens inside the render effect */
readonly update: Statement[];
/** Stuff that happens after the render effect (control blocks, dynamic elements, bindings, actions, etc) */
readonly after_update: Statement[];
+ /** Expressions used inside the render effect */
+ readonly expressions: Expression[];
/** The HTML template string */
readonly template: Array;
readonly locations: SourceLocation[];
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 c59a5544df..6d9dac8a33 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/utils.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/utils.js
@@ -1,10 +1,10 @@
-/** @import { ArrowFunctionExpression, Expression, FunctionDeclaration, FunctionExpression, Identifier, Pattern, PrivateIdentifier, Statement } from 'estree' */
-/** @import { AST, Binding } from '#compiler' */
+/** @import { ArrowFunctionExpression, AssignmentExpression, Expression, FunctionDeclaration, FunctionExpression, Identifier, Node, Pattern, UpdateExpression } from 'estree' */
+/** @import { Binding } from '#compiler' */
/** @import { ClientTransformState, ComponentClientTransformState, ComponentContext } from './types.js' */
/** @import { Analysis } from '../../types.js' */
/** @import { Scope } from '../../scope.js' */
-import * as b from '../../../utils/builders.js';
-import { extract_identifiers, is_simple_expression } from '../../../utils/ast.js';
+import * as b from '#compiler/builders';
+import { is_simple_expression } from '../../../utils/ast.js';
import {
PROPS_IS_LAZY_INITIAL,
PROPS_IS_IMMUTABLE,
@@ -13,7 +13,8 @@ import {
PROPS_IS_BINDABLE
} from '../../../../constants.js';
import { dev } from '../../../state.js';
-import { get_value } from './visitors/shared/declarations.js';
+import { walk } from 'zimmerframe';
+import { validate_mutation } from './visitors/shared/utils.js';
/**
* @param {Binding} binding
@@ -45,14 +46,6 @@ export function build_getter(node, state) {
return node;
}
-/**
- * @param {Expression} value
- * @param {Expression} previous
- */
-export function build_proxy_reassignment(value, previous) {
- return dev ? b.call('$.proxy', value, b.null, previous) : b.call('$.proxy', value);
-}
-
/**
* @param {FunctionDeclaration | FunctionExpression | ArrowFunctionExpression} node
* @param {ComponentContext} context
@@ -118,6 +111,30 @@ function get_hoisted_params(node, context) {
}
}
}
+
+ if (dev) {
+ // this is a little hacky, but necessary for ownership validation
+ // to work inside hoisted event handlers
+
+ /**
+ * @param {AssignmentExpression | UpdateExpression} node
+ * @param {{ next: () => void, stop: () => void }} context
+ */
+ function visit(node, { next, stop }) {
+ if (validate_mutation(node, /** @type {any} */ (context), node) !== node) {
+ params.push(b.id('$$ownership_validator'));
+ stop();
+ } else {
+ next();
+ }
+ }
+
+ walk(/** @type {Node} */ (node), null, {
+ AssignmentExpression: visit,
+ UpdateExpression: visit
+ });
+ }
+
return params;
}
@@ -269,41 +286,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/AnimateDirective.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AnimateDirective.js
index 510f32cde5..16f9735370 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AnimateDirective.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AnimateDirective.js
@@ -1,7 +1,7 @@
/** @import { Expression } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */
-import * as b from '../../../../utils/builders.js';
+import * as b from '#compiler/builders';
import { parse_directive_name } from './shared/utils.js';
/**
@@ -11,7 +11,7 @@ import { parse_directive_name } from './shared/utils.js';
export function AnimateDirective(node, context) {
const expression =
node.expression === null
- ? b.literal(null)
+ ? b.null
: b.thunk(/** @type {Expression} */ (context.visit(node.expression)));
// in after_update to ensure it always happens after bind:this
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js
index 0c70f7e00c..e3f9450050 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js
@@ -1,16 +1,16 @@
-/** @import { Location } from 'locate-character' */
-/** @import { AssignmentExpression, AssignmentOperator, Expression, Identifier, Literal, MemberExpression, Pattern } from 'estree' */
+/** @import { AssignmentExpression, AssignmentOperator, Expression, Identifier, Pattern } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { Context } from '../types.js' */
-import * as b from '../../../../utils/builders.js';
+import * as b from '#compiler/builders';
import {
build_assignment_value,
get_attribute_expression,
is_event_attribute
} from '../../../../utils/ast.js';
-import { dev, filename, is_ignored, locate_node, locator } from '../../../../state.js';
-import { build_proxy_reassignment, should_proxy } from '../utils.js';
+import { dev, locate_node } from '../../../../state.js';
+import { should_proxy } from '../utils.js';
import { visit_assignment_expression } from '../../shared/assignments.js';
+import { validate_mutation } from './shared/utils.js';
/**
* @param {AssignmentExpression} node
@@ -21,9 +21,7 @@ export function AssignmentExpression(node, context) {
visit_assignment_expression(node, context, build_assignment) ?? context.next()
);
- return is_ignored(node, 'ownership_invalid_mutation')
- ? b.call('$.skip_ownership_validation', b.thunk(expression))
- : expression;
+ return validate_mutation(node, context, expression);
}
/**
@@ -65,21 +63,12 @@ function build_assignment(operator, left, right, context) {
context.visit(build_assignment_value(operator, left, right))
);
- if (
+ const needs_proxy =
private_state.kind === 'state' &&
is_non_coercive_operator(operator) &&
- should_proxy(value, context.state.scope)
- ) {
- value = build_proxy_reassignment(value, b.member(b.this, private_state.id));
- }
-
- if (context.state.in_constructor) {
- // inside the constructor, we can assign to `this.#foo.v` rather than using `$.set`,
- // since nothing is tracking the signal at this point
- return b.assignment(operator, /** @type {Pattern} */ (context.visit(left)), value);
- }
+ should_proxy(value, context.state.scope);
- return b.call('$.set', left, value);
+ return b.call('$.set', left, value, needs_proxy && b.true);
}
}
@@ -113,19 +102,18 @@ function build_assignment(operator, left, right, context) {
context.visit(build_assignment_value(operator, left, right))
);
- if (
+ return transform.assign(
+ object,
+ value,
!is_primitive &&
- binding.kind !== 'prop' &&
- binding.kind !== 'bindable_prop' &&
- binding.kind !== 'raw_state' &&
- context.state.analysis.runes &&
- should_proxy(right, context.state.scope) &&
- is_non_coercive_operator(operator)
- ) {
- value = build_proxy_reassignment(value, object);
- }
-
- return transform.assign(object, value);
+ binding.kind !== 'prop' &&
+ binding.kind !== 'bindable_prop' &&
+ binding.kind !== 'raw_state' &&
+ binding.kind !== 'store_sub' &&
+ context.state.analysis.runes &&
+ should_proxy(right, context.state.scope) &&
+ is_non_coercive_operator(operator)
+ );
}
// mutation
@@ -176,7 +164,9 @@ function build_assignment(operator, left, right, context) {
path.at(-1) === 'SvelteComponent' ||
(path.at(-1) === 'ArrowFunctionExpression' &&
path.at(-2) === 'SequenceExpression' &&
- (path.at(-3) === 'Component' || path.at(-3) === 'SvelteComponent'))
+ (path.at(-3) === 'Component' ||
+ path.at(-3) === 'SvelteComponent' ||
+ path.at(-3) === 'BindDirective'))
) {
should_transform = false;
}
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..96a4addb72 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 * as b from '../../../../utils/builders.js';
-import { create_derived_block_argument } from '../utils.js';
+/** @import { ComponentClientTransformState, ComponentContext } from '../types' */
+import { extract_identifiers } from '../../../../utils/ast.js';
+import * as b from '#compiler/builders';
+import { create_derived } from '../utils.js';
+import { get_value } from './shared/declarations.js';
/**
* @param {AST.AwaitBlock} node
@@ -58,10 +60,45 @@ export function AwaitBlock(node, context) {
expression,
node.pending
? b.arrow([b.id('$$anchor')], /** @type {BlockStatement} */ (context.visit(node.pending)))
- : b.literal(null),
+ : b.null,
then_block,
catch_block
)
)
);
}
+
+/**
+ * @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/BinaryExpression.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/BinaryExpression.js
index c8c54a5a59..18028fa071 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/BinaryExpression.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/BinaryExpression.js
@@ -1,7 +1,7 @@
/** @import { Expression, BinaryExpression } from 'estree' */
/** @import { ComponentContext } from '../types' */
import { dev } from '../../../../state.js';
-import * as b from '../../../../utils/builders.js';
+import * as b from '#compiler/builders';
/**
* @param {BinaryExpression} node
@@ -16,7 +16,7 @@ export function BinaryExpression(node, context) {
'$.strict_equals',
/** @type {Expression} */ (context.visit(node.left)),
/** @type {Expression} */ (context.visit(node.right)),
- operator === '!==' && b.literal(false)
+ operator === '!==' && b.false
);
}
@@ -25,7 +25,7 @@ export function BinaryExpression(node, context) {
'$.equals',
/** @type {Expression} */ (context.visit(node.left)),
/** @type {Expression} */ (context.visit(node.right)),
- operator === '!=' && b.literal(false)
+ operator === '!=' && b.false
);
}
}
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/BindDirective.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/BindDirective.js
index 0a49e89cfe..506fd4aafd 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/BindDirective.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/BindDirective.js
@@ -3,7 +3,7 @@
/** @import { ComponentContext } from '../types' */
import { dev, is_ignored } from '../../../../state.js';
import { is_text_attribute } from '../../../../utils/ast.js';
-import * as b from '../../../../utils/builders.js';
+import * as b from '#compiler/builders';
import { binding_properties } from '../../../bindings.js';
import { build_attribute_value } from './shared/element.js';
import { build_bind_this, validate_binding } from './shared/utils.js';
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/BlockStatement.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/BlockStatement.js
index 5bfc8a3ef9..d1c0978a81 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/BlockStatement.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/BlockStatement.js
@@ -1,7 +1,7 @@
-/** @import { ArrowFunctionExpression, BlockStatement, CallExpression, Expression, FunctionDeclaration, FunctionExpression, Statement } from 'estree' */
+/** @import { ArrowFunctionExpression, BlockStatement, Expression, FunctionDeclaration, FunctionExpression, Statement } from 'estree' */
/** @import { ComponentContext } from '../types' */
import { add_state_transformers } from './shared/declarations.js';
-import * as b from '../../../../utils/builders.js';
+import * as b from '#compiler/builders';
/**
* @param {BlockStatement} node
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/BreakStatement.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/BreakStatement.js
index 66b66c64f2..daa54018c0 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/BreakStatement.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/BreakStatement.js
@@ -1,6 +1,6 @@
/** @import { BreakStatement } from 'estree' */
/** @import { ComponentContext } from '../types' */
-import * as b from '../../../../utils/builders.js';
+import * as b from '#compiler/builders';
/**
* @param {BreakStatement} node
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/CallExpression.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/CallExpression.js
index 7a3057451a..b110f8eae8 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/CallExpression.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/CallExpression.js
@@ -1,7 +1,7 @@
/** @import { CallExpression, Expression } from 'estree' */
/** @import { Context } from '../types' */
import { dev, is_ignored } from '../../../../state.js';
-import * as b from '../../../../utils/builders.js';
+import * as b from '#compiler/builders';
import { get_rune } from '../../../scope.js';
import { transform_inspect_rune } from '../../utils.js';
@@ -44,7 +44,8 @@ export function CallExpression(node, context) {
node.callee.property.type === 'Identifier' &&
['debug', 'dir', 'error', 'group', 'groupCollapsed', 'info', 'log', 'trace', 'warn'].includes(
node.callee.property.name
- )
+ ) &&
+ node.arguments.some((arg) => arg.type !== 'Literal') // TODO more cases?
) {
return b.call(
node.callee,
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 7b3a9a4d0e..f3ebd94069 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
@@ -1,11 +1,9 @@
/** @import { ClassBody, Expression, Identifier, Literal, MethodDefinition, PrivateIdentifier, PropertyDefinition } from 'estree' */
-/** @import { } from '#compiler' */
/** @import { Context, StateField } from '../types' */
-import { dev, is_ignored } from '../../../../state.js';
-import * as b from '../../../../utils/builders.js';
+import * as b from '#compiler/builders';
import { regex_invalid_identifier_chars } from '../../../patterns.js';
import { get_rune } from '../../../scope.js';
-import { build_proxy_reassignment, should_proxy } from '../utils.js';
+import { should_proxy } from '../utils.js';
/**
* @param {ClassBody} node
@@ -142,39 +140,17 @@ export function ClassBody(node, context) {
// get foo() { return this.#foo; }
body.push(b.method('get', definition.key, [], [b.return(b.call('$.get', member))]));
- if (field.kind === 'state') {
- // set foo(value) { this.#foo = value; }
- const value = b.id('value');
- const prev = b.member(b.this, field.id);
-
- body.push(
- b.method(
- 'set',
- definition.key,
- [value],
- [b.stmt(b.call('$.set', member, build_proxy_reassignment(value, prev)))]
- )
- );
- }
-
- if (field.kind === 'raw_state') {
- // set foo(value) { this.#foo = value; }
- const value = b.id('value');
- body.push(
- b.method('set', definition.key, [value], [b.stmt(b.call('$.set', member, value))])
- );
- }
+ // set foo(value) { this.#foo = value; }
+ const val = b.id('value');
- if (dev && (field.kind === 'derived' || field.kind === 'derived_by')) {
- body.push(
- b.method(
- 'set',
- definition.key,
- [b.id('_')],
- [b.throw_error(`Cannot update a derived property ('${name}')`)]
- )
- );
- }
+ body.push(
+ b.method(
+ 'set',
+ definition.key,
+ [val],
+ [b.stmt(b.call('$.set', member, val, field.kind === 'state' && b.true))]
+ )
+ );
}
continue;
}
@@ -183,34 +159,6 @@ export function ClassBody(node, context) {
body.push(/** @type {MethodDefinition} **/ (context.visit(definition, child_state)));
}
- if (dev && public_state.size > 0) {
- // add an `[$.ADD_OWNER]` method so that a class with state fields can widen ownership
- body.push(
- b.method(
- '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
- )
- )
- ),
- true
- )
- );
- }
-
return { ...node, body };
}
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Component.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Component.js
index a10a3da6e3..783bc38e3c 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Component.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Component.js
@@ -1,7 +1,7 @@
/** @import { Expression } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */
-import * as b from '../../../../utils/builders.js';
+import * as b from '#compiler/builders';
import { build_component } from './shared/component.js';
/**
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ConstTag.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ConstTag.js
index 7e33aea435..2f3c0b3d0e 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ConstTag.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ConstTag.js
@@ -3,7 +3,7 @@
/** @import { ComponentContext } from '../types' */
import { dev } from '../../../../state.js';
import { extract_identifiers } from '../../../../utils/ast.js';
-import * as b from '../../../../utils/builders.js';
+import * as b from '#compiler/builders';
import { create_derived } from '../utils.js';
import { get_value } from './shared/declarations.js';
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/DebugTag.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/DebugTag.js
index d2697fd039..ef9a070859 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/DebugTag.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/DebugTag.js
@@ -1,7 +1,7 @@
/** @import { Expression} from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */
-import * as b from '../../../../utils/builders.js';
+import * as b from '#compiler/builders';
/**
* @param {AST.DebugTag} node
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..e5aee24765 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
@@ -11,7 +11,7 @@ import {
} from '../../../../../constants.js';
import { dev } from '../../../../state.js';
import { extract_paths, object } from '../../../../utils/ast.js';
-import * as b from '../../../../utils/builders.js';
+import * as b from '#compiler/builders';
import { build_getter } from '../utils.js';
import { get_value } from './shared/declarations.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);
@@ -134,7 +143,8 @@ export function EachBlock(node, context) {
const child_state = {
...context.state,
- transform: { ...context.state.transform }
+ transform: { ...context.state.transform },
+ store_to_invalidate
};
/** The state used when generating the key function, if necessary */
@@ -195,7 +205,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 +217,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 +293,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/ExportNamedDeclaration.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ExportNamedDeclaration.js
index cab7f90c3d..16e400d50c 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ExportNamedDeclaration.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ExportNamedDeclaration.js
@@ -1,6 +1,6 @@
/** @import { ExportNamedDeclaration } from 'estree' */
/** @import { ComponentContext } from '../types' */
-import * as b from '../../../../utils/builders.js';
+import * as b from '#compiler/builders';
/**
* @param {ExportNamedDeclaration} node
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ExpressionStatement.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ExpressionStatement.js
index 0424e595be..859842ebc3 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ExpressionStatement.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ExpressionStatement.js
@@ -1,6 +1,6 @@
/** @import { Expression, ExpressionStatement } from 'estree' */
/** @import { ComponentContext } from '../types' */
-import * as b from '../../../../utils/builders.js';
+import * as b from '#compiler/builders';
import { get_rune } from '../../../scope.js';
/**
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Fragment.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Fragment.js
index 0e6ea29614..b6dca0779a 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Fragment.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Fragment.js
@@ -4,7 +4,7 @@
/** @import { ComponentClientTransformState, ComponentContext } from '../types' */
import { TEMPLATE_FRAGMENT, TEMPLATE_USE_IMPORT_NODE } from '../../../../../constants.js';
import { dev } from '../../../../state.js';
-import * as b from '../../../../utils/builders.js';
+import * as b from '#compiler/builders';
import { sanitize_template_string } from '../../../../utils/sanitize_template_string.js';
import { clean_nodes, infer_namespace } from '../../utils.js';
import { process_children } from './shared/fragment.js';
@@ -48,7 +48,9 @@ export function Fragment(node, context) {
const is_single_element = trimmed.length === 1 && trimmed[0].type === 'RegularElement';
const is_single_child_not_needing_template =
trimmed.length === 1 &&
- (trimmed[0].type === 'SvelteFragment' || trimmed[0].type === 'TitleElement');
+ (trimmed[0].type === 'SvelteFragment' ||
+ trimmed[0].type === 'TitleElement' ||
+ (trimmed[0].type === 'IfBlock' && trimmed[0].elseif));
const template_name = context.state.scope.root.unique('root'); // TODO infer name from parent
@@ -61,9 +63,9 @@ export function Fragment(node, context) {
/** @type {ComponentClientTransformState} */
const state = {
...context.state,
- before_init: [],
init: [],
update: [],
+ expressions: [],
after_update: [],
template: [],
locations: [],
@@ -124,18 +126,13 @@ export function Fragment(node, context) {
add_template(template_name, args);
- body.push(b.var(id, b.call(template_name)), ...state.before_init, ...state.init);
+ body.push(b.var(id, b.call(template_name)));
close = b.stmt(b.call('$.append', b.id('$$anchor'), id));
} else if (is_single_child_not_needing_template) {
context.visit(trimmed[0], state);
- body.push(...state.before_init, ...state.init);
} else if (trimmed.length === 1 && trimmed[0].type === 'Text') {
const id = b.id(context.state.scope.generate('text'));
- body.push(
- b.var(id, b.call('$.text', b.literal(trimmed[0].data))),
- ...state.before_init,
- ...state.init
- );
+ body.push(b.var(id, b.call('$.text', b.literal(trimmed[0].data))));
close = b.stmt(b.call('$.append', b.id('$$anchor'), id));
} else if (trimmed.length > 0) {
const id = b.id(context.state.scope.generate('fragment'));
@@ -153,7 +150,7 @@ export function Fragment(node, context) {
state
});
- body.push(b.var(id, b.call('$.text')), ...state.before_init, ...state.init);
+ body.push(b.var(id, b.call('$.text')));
close = b.stmt(b.call('$.append', b.id('$$anchor'), id));
} else {
if (is_standalone) {
@@ -182,15 +179,13 @@ export function Fragment(node, context) {
close = b.stmt(b.call('$.append', b.id('$$anchor'), id));
}
-
- body.push(...state.before_init, ...state.init);
}
- } else {
- body.push(...state.before_init, ...state.init);
}
+ body.push(...state.init);
+
if (state.update.length > 0) {
- body.push(build_render_statement(state.update));
+ body.push(build_render_statement(state));
}
body.push(...state.after_update);
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/FunctionDeclaration.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/FunctionDeclaration.js
index ed8fefc6ba..5dc8fa5cf9 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/FunctionDeclaration.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/FunctionDeclaration.js
@@ -1,7 +1,7 @@
/** @import { FunctionDeclaration } from 'estree' */
/** @import { ComponentContext } from '../types' */
import { build_hoisted_params } from '../utils.js';
-import * as b from '../../../../utils/builders.js';
+import * as b from '#compiler/builders';
/**
* @param {FunctionDeclaration} node
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/HtmlTag.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/HtmlTag.js
index 32439879de..a69b9cfe70 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/HtmlTag.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/HtmlTag.js
@@ -2,7 +2,7 @@
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */
import { is_ignored } from '../../../../state.js';
-import * as b from '../../../../utils/builders.js';
+import * as b from '#compiler/builders';
/**
* @param {AST.HtmlTag} node
@@ -11,17 +11,22 @@ import * as b from '../../../../utils/builders.js';
export function HtmlTag(node, context) {
context.state.template.push('');
- // push into init, so that bindings run afterwards, which might trigger another run and override hydration
- context.state.init.push(
- b.stmt(
- b.call(
- '$.html',
- context.state.node,
- b.thunk(/** @type {Expression} */ (context.visit(node.expression))),
- b.literal(context.state.metadata.namespace === 'svg'),
- b.literal(context.state.metadata.namespace === 'mathml'),
- is_ignored(node, 'hydration_html_changed') && b.true
- )
+ const expression = /** @type {Expression} */ (context.visit(node.expression));
+
+ const is_svg = context.state.metadata.namespace === 'svg';
+ const is_mathml = context.state.metadata.namespace === 'mathml';
+
+ const statement = b.stmt(
+ b.call(
+ '$.html',
+ context.state.node,
+ b.thunk(expression),
+ is_svg && b.true,
+ is_mathml && b.true,
+ is_ignored(node, 'hydration_html_changed') && b.true
)
);
+
+ // push into init, so that bindings run afterwards, which might trigger another run and override hydration
+ context.state.init.push(statement);
}
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Identifier.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Identifier.js
index ae62909eff..b01ed01bd7 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Identifier.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Identifier.js
@@ -1,7 +1,7 @@
/** @import { Identifier, Node } from 'estree' */
/** @import { Context } from '../types' */
import is_reference from 'is-reference';
-import * as b from '../../../../utils/builders.js';
+import * as b from '#compiler/builders';
import { build_getter } from '../utils.js';
/**
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/IfBlock.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/IfBlock.js
index d658f9eaf8..c650a1e15c 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/IfBlock.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/IfBlock.js
@@ -1,7 +1,7 @@
/** @import { BlockStatement, Expression } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */
-import * as b from '../../../../utils/builders.js';
+import * as b from '#compiler/builders';
/**
* @param {AST.IfBlock} node
@@ -19,29 +19,28 @@ export function IfBlock(node, context) {
let alternate_id;
if (node.alternate) {
- const alternate = /** @type {BlockStatement} */ (context.visit(node.alternate));
alternate_id = context.state.scope.generate('alternate');
- statements.push(b.var(b.id(alternate_id), b.arrow([b.id('$$anchor')], alternate)));
+ const alternate = /** @type {BlockStatement} */ (context.visit(node.alternate));
+ const nodes = node.alternate.nodes;
+
+ let alternate_args = [b.id('$$anchor')];
+ if (nodes.length === 1 && nodes[0].type === 'IfBlock' && nodes[0].elseif) {
+ alternate_args.push(b.id('$$elseif'));
+ }
+
+ statements.push(b.var(b.id(alternate_id), b.arrow(alternate_args, alternate)));
}
/** @type {Expression[]} */
const args = [
- context.state.node,
+ node.elseif ? b.id('$$anchor') : context.state.node,
b.arrow(
[b.id('$$render')],
b.block([
b.if(
/** @type {Expression} */ (context.visit(node.test)),
b.stmt(b.call(b.id('$$render'), b.id(consequent_id))),
- alternate_id
- ? b.stmt(
- b.call(
- b.id('$$render'),
- b.id(alternate_id),
- node.alternate ? b.literal(false) : undefined
- )
- )
- : undefined
+ alternate_id ? b.stmt(b.call(b.id('$$render'), b.id(alternate_id), b.false)) : undefined
)
])
)
@@ -69,7 +68,7 @@ export function IfBlock(node, context) {
// ...even though they're logically equivalent. In the first case, the
// transition will only play when `y` changes, but in the second it
// should play when `x` or `y` change — both are considered 'local'
- args.push(b.literal(true));
+ args.push(b.id('$$elseif'));
}
statements.push(b.stmt(b.call('$.if', ...args)));
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ImportDeclaration.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ImportDeclaration.js
index 29700246d4..b572e1d17f 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ImportDeclaration.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ImportDeclaration.js
@@ -1,6 +1,6 @@
/** @import { ImportDeclaration } from 'estree' */
/** @import { ComponentContext } from '../types' */
-import * as b from '../../../../utils/builders.js';
+import * as b from '#compiler/builders';
/**
* @param {ImportDeclaration} node
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/KeyBlock.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/KeyBlock.js
index a013827f60..7d6a8b0006 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/KeyBlock.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/KeyBlock.js
@@ -1,7 +1,7 @@
/** @import { Expression } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */
-import * as b from '../../../../utils/builders.js';
+import * as b from '#compiler/builders';
/**
* @param {AST.KeyBlock} node
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/LabeledStatement.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/LabeledStatement.js
index 3c8f57f46b..8d24d260c5 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/LabeledStatement.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/LabeledStatement.js
@@ -1,9 +1,7 @@
-/** @import { Location } from 'locate-character' */
/** @import { Expression, LabeledStatement, Statement } from 'estree' */
/** @import { ReactiveStatement } from '#compiler' */
/** @import { ComponentContext } from '../types' */
-import { dev, is_ignored, locator } from '../../../../state.js';
-import * as b from '../../../../utils/builders.js';
+import * as b from '#compiler/builders';
import { build_getter } from '../utils.js';
/**
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/LetDirective.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/LetDirective.js
index e174073a26..abdbc381d9 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/LetDirective.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/LetDirective.js
@@ -1,7 +1,7 @@
/** @import { Expression } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */
-import * as b from '../../../../utils/builders.js';
+import * as b from '#compiler/builders';
import { create_derived } from '../utils.js';
/**
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/MemberExpression.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/MemberExpression.js
index 501ecda555..ab88345ddd 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/MemberExpression.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/MemberExpression.js
@@ -1,6 +1,6 @@
/** @import { MemberExpression } from 'estree' */
/** @import { Context } from '../types' */
-import * as b from '../../../../utils/builders.js';
+import * as b from '#compiler/builders';
/**
* @param {MemberExpression} node
@@ -11,7 +11,9 @@ export function MemberExpression(node, context) {
if (node.property.type === 'PrivateIdentifier') {
const field = context.state.private_state.get(node.property.name);
if (field) {
- return context.state.in_constructor ? b.member(node, 'v') : b.call('$.get', node);
+ return context.state.in_constructor && (field.kind === 'raw_state' || field.kind === 'state')
+ ? b.member(node, 'v')
+ : b.call('$.get', node);
}
}
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/OnDirective.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/OnDirective.js
index 7c2b1209e9..7a66a8ecbb 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/OnDirective.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/OnDirective.js
@@ -1,6 +1,6 @@
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */
-import * as b from '../../../../utils/builders.js';
+import * as b from '#compiler/builders';
import { build_event, build_event_handler } from './shared/events.js';
const modifiers = [
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Program.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Program.js
index 29403ca6ed..07342da314 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Program.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Program.js
@@ -1,7 +1,7 @@
/** @import { Expression, ImportDeclaration, MemberExpression, Program } from 'estree' */
/** @import { ComponentContext } from '../types' */
import { build_getter, is_prop_source } from '../utils.js';
-import * as b from '../../../../utils/builders.js';
+import * as b from '#compiler/builders';
import { add_state_transformers } from './shared/declarations.js';
/**
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 ffd06dfd86..7468fcbbc7 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
@@ -1,4 +1,4 @@
-/** @import { Expression, ExpressionStatement, Identifier, MemberExpression, ObjectExpression, Statement } from 'estree' */
+/** @import { ArrayExpression, Expression, ExpressionStatement, Identifier, MemberExpression, ObjectExpression } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { SourceLocation } from '#shared' */
/** @import { ComponentClientTransformState, ComponentContext } from '../types' */
@@ -13,23 +13,24 @@ import {
import { escape_html } from '../../../../../escaping.js';
import { dev, is_ignored, locator } from '../../../../state.js';
import { is_event_attribute, is_text_attribute } from '../../../../utils/ast.js';
-import * as b from '../../../../utils/builders.js';
+import * as b from '#compiler/builders';
import { is_custom_element_node } from '../../../nodes.js';
import { clean_nodes, determine_namespace_for_children } from '../../utils.js';
-import { build_getter, create_derived } from '../utils.js';
+import { build_getter } from '../utils.js';
import {
get_attribute_name,
build_attribute_value,
- build_class_directives,
- build_style_directives,
- build_set_attributes
+ build_set_attributes,
+ build_set_class,
+ build_set_style
} from './shared/element.js';
import { process_children } from './shared/fragment.js';
import {
build_render_statement,
build_template_chunk,
- build_update,
- build_update_assignment
+ build_update_assignment,
+ get_expression_id,
+ memoize_expression
} from './shared/utils.js';
import { visit_event_attribute } from './shared/events.js';
@@ -214,21 +215,17 @@ export function RegularElement(node, context) {
const node_id = context.state.node;
- // Then do attributes
- let is_attributes_reactive = has_spread;
-
if (has_spread) {
const attributes_id = b.id(context.state.scope.generate('attributes'));
build_set_attributes(
attributes,
+ class_directives,
+ style_directives,
context,
node,
node_id,
- attributes_id,
- (node.metadata.svg || node.metadata.mathml || is_custom_element_node(node)) && b.true,
- node.name.includes('-') && b.true,
- context.state
+ attributes_id
);
// If value binding exists, that one takes care of calling $.init_select
@@ -269,13 +266,24 @@ export function RegularElement(node, context) {
continue;
}
+ const name = get_attribute_name(node, attribute);
+
if (
!is_custom_element &&
!cannot_be_set_statically(attribute.name) &&
- (attribute.value === true || is_text_attribute(attribute))
+ (attribute.value === true || is_text_attribute(attribute)) &&
+ (name !== 'class' || class_directives.length === 0) &&
+ (name !== 'style' || style_directives.length === 0)
) {
- const name = get_attribute_name(node, attribute);
- const value = is_text_attribute(attribute) ? attribute.value[0].data : true;
+ let value = is_text_attribute(attribute) ? attribute.value[0].data : true;
+
+ if (name === 'class' && node.metadata.scoped && context.state.analysis.css.hash) {
+ if (value === true || value === '') {
+ value = context.state.analysis.css.hash;
+ } else {
+ value += ' ' + context.state.analysis.css.hash;
+ }
+ }
if (name !== 'class' || value) {
context.state.template.push(
@@ -286,23 +294,28 @@ export function RegularElement(node, context) {
}`
);
}
- continue;
- }
-
- const is = is_custom_element
- ? build_custom_element_attribute_update_assignment(node_id, attribute, context)
- : build_element_attribute_update_assignment(node, node_id, attribute, attributes, context);
- if (is) is_attributes_reactive = true;
- }
- }
+ } else if (name === 'autofocus') {
+ let { value } = build_attribute_value(attribute.value, context);
+ context.state.init.push(b.stmt(b.call('$.autofocus', node_id, value)));
+ } else if (name === 'class') {
+ const is_html = context.state.metadata.namespace === 'html' && node.name !== 'svg';
+ build_set_class(node, node_id, attribute, class_directives, context, is_html);
+ } else if (name === 'style') {
+ build_set_style(node_id, attribute, style_directives, context);
+ } else if (is_custom_element) {
+ build_custom_element_attribute_update_assignment(node_id, attribute, context);
+ } else {
+ const { value, has_state } = build_attribute_value(
+ attribute.value,
+ context,
+ (value, metadata) => (metadata.has_call ? get_expression_id(context.state, value) : value)
+ );
- // class/style directives must be applied last since they could override class/style attributes
- build_class_directives(class_directives, node_id, context, is_attributes_reactive);
- build_style_directives(style_directives, node_id, context, is_attributes_reactive);
+ const update = build_element_attribute_update(node, node_id, name, value, attributes);
- // 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)));
+ (has_state ? context.state.update : context.state.init).push(b.stmt(update));
+ }
+ }
}
if (
@@ -368,15 +381,14 @@ export function RegularElement(node, context) {
trimmed.some((node) => node.type === 'ExpressionTag');
if (use_text_content) {
- child_state.init.push(
- b.stmt(
- b.assignment(
- '=',
- b.member(context.state.node, 'textContent'),
- build_template_chunk(trimmed, context.visit, child_state).value
- )
- )
- );
+ const { value } = build_template_chunk(trimmed, context.visit, child_state);
+ const empty_string = value.type === 'Literal' && value.value === '';
+
+ if (!empty_string) {
+ child_state.init.push(
+ b.stmt(b.assignment('=', b.member(context.state.node, 'textContent'), value))
+ );
+ }
} else {
/** @type {Expression} */
let arg = context.state.node;
@@ -409,7 +421,7 @@ export function RegularElement(node, context) {
b.block([
...child_state.init,
...element_state.init,
- child_state.update.length > 0 ? build_render_statement(child_state.update) : b.empty,
+ child_state.update.length > 0 ? build_render_statement(child_state) : b.empty,
...child_state.after_update,
...element_state.after_update
])
@@ -496,6 +508,56 @@ function setup_select_synchronization(value_binding, context) {
);
}
+/**
+ * @param {AST.ClassDirective[]} class_directives
+ * @param {ComponentContext} context
+ * @return {ObjectExpression | Identifier}
+ */
+export function build_class_directives_object(class_directives, context) {
+ let properties = [];
+ let has_call_or_state = false;
+
+ for (const d of class_directives) {
+ const expression = /** @type Expression */ (context.visit(d.expression));
+ properties.push(b.init(d.name, expression));
+ has_call_or_state ||= d.metadata.expression.has_call || d.metadata.expression.has_state;
+ }
+
+ const directives = b.object(properties);
+
+ return has_call_or_state ? get_expression_id(context.state, directives) : directives;
+}
+
+/**
+ * @param {AST.StyleDirective[]} style_directives
+ * @param {ComponentContext} context
+ * @return {ObjectExpression | ArrayExpression}}
+ */
+export function build_style_directives_object(style_directives, context) {
+ let normal_properties = [];
+ let important_properties = [];
+
+ for (const directive of style_directives) {
+ const expression =
+ directive.value === true
+ ? build_getter({ name: directive.name, type: 'Identifier' }, context.state)
+ : build_attribute_value(directive.value, context, (value, metadata) =>
+ metadata.has_call ? get_expression_id(context.state, value) : value
+ ).value;
+ const property = b.init(directive.name, expression);
+
+ if (directive.modifiers.includes('important')) {
+ important_properties.push(property);
+ } else {
+ normal_properties.push(property);
+ }
+ }
+
+ return important_properties.length
+ ? b.array([b.object(normal_properties), b.object(important_properties)])
+ : b.object(normal_properties);
+}
+
/**
* Serializes an assignment to an element property by adding relevant statements to either only
* the init or the the init and update arrays, depending on whether or not the value is dynamic.
@@ -520,69 +582,29 @@ function setup_select_synchronization(value_binding, context) {
* Returns true if attribute is deemed reactive, false otherwise.
* @param {AST.RegularElement} element
* @param {Identifier} node_id
- * @param {AST.Attribute} attribute
+ * @param {string} name
+ * @param {Expression} value
* @param {Array} attributes
- * @param {ComponentContext} context
- * @returns {boolean}
*/
-function build_element_attribute_update_assignment(
- element,
- node_id,
- attribute,
- attributes,
- context
-) {
- const state = context.state;
- const name = get_attribute_name(element, attribute);
- const is_svg = context.state.metadata.namespace === 'svg' || element.name === 'svg';
- const is_mathml = context.state.metadata.namespace === 'mathml';
- let { has_call, value } = build_attribute_value(attribute.value, context);
-
- if (name === 'autofocus') {
- state.init.push(b.stmt(b.call('$.autofocus', node_id, value)));
- return false;
- }
-
- // Special case for Firefox who needs it set as a property in order to work
+function build_element_attribute_update(element, node_id, name, value, attributes) {
if (name === 'muted') {
- state.init.push(b.stmt(b.assignment('=', b.member(node_id, b.id('muted')), value)));
- return false;
+ // Special case for Firefox who needs it set as a property in order to work
+ return b.assignment('=', b.member(node_id, b.id('muted')), value);
}
- /** @type {Statement} */
- let update;
+ if (name === 'value') {
+ return b.call('$.set_value', node_id, value);
+ }
- if (name === 'class') {
- if (attribute.metadata.needs_clsx) {
- value = b.call('$.clsx', value);
- }
+ if (name === 'checked') {
+ return b.call('$.set_checked', node_id, value);
+ }
- if (attribute.metadata.expression.has_state && has_call) {
- // ensure we're not creating a separate template effect for this so that
- // potential class directives are added to the same effect and therefore always apply
- const id = b.id(state.scope.generate('class_derived'));
- state.init.push(b.const(id, create_derived(state, b.thunk(value))));
- value = b.call('$.get', id);
- has_call = false;
- }
+ if (name === 'selected') {
+ return b.call('$.set_selected', node_id, value);
+ }
- update = b.stmt(
- b.call(
- is_svg ? '$.set_svg_class' : is_mathml ? '$.set_mathml_class' : '$.set_class',
- node_id,
- value,
- attribute.metadata.needs_clsx && context.state.analysis.css.hash
- ? b.literal(context.state.analysis.css.hash)
- : undefined
- )
- );
- } else if (name === 'value') {
- update = b.stmt(b.call('$.set_value', node_id, value));
- } else if (name === 'checked') {
- update = b.stmt(b.call('$.set_checked', node_id, value));
- } else if (name === 'selected') {
- update = b.stmt(b.call('$.set_selected', node_id, value));
- } else if (
+ if (
// If we would just set the defaultValue property, it would override the value property,
// because it is set in the template which implicitly means it's also setting the default value,
// and if one updates the default value while the input is pristine it will also update the
@@ -593,84 +615,49 @@ function build_element_attribute_update_assignment(
) ||
(element.name === 'textarea' && element.fragment.nodes.length > 0))
) {
- update = b.stmt(b.call('$.set_default_value', node_id, value));
- } else if (
+ return b.call('$.set_default_value', node_id, value);
+ }
+
+ if (
// See defaultValue comment
name === 'defaultChecked' &&
attributes.some(
(attr) => attr.type === 'Attribute' && attr.name === 'checked' && attr.value === true
)
) {
- update = b.stmt(b.call('$.set_default_checked', node_id, value));
- } else if (is_dom_property(name)) {
- update = b.stmt(b.assignment('=', b.member(node_id, name), value));
- } else {
- if (name === 'style' && attribute.metadata.expression.has_state && has_call) {
- // ensure we're not creating a separate template effect for this so that
- // potential style directives are added to the same effect and therefore always apply
- const id = b.id(state.scope.generate('style_derived'));
- state.init.push(b.const(id, create_derived(state, b.thunk(value))));
- value = b.call('$.get', id);
- has_call = false;
- }
- const callee = name.startsWith('xlink') ? '$.set_xlink_attribute' : '$.set_attribute';
- update = b.stmt(
- b.call(
- callee,
- node_id,
- b.literal(name),
- value,
- is_ignored(element, 'hydration_attribute_changed') && b.true
- )
- );
+ return b.call('$.set_default_checked', node_id, value);
}
- if (attribute.metadata.expression.has_state) {
- if (has_call) {
- state.init.push(build_update(update));
- } else {
- state.update.push(update);
- }
- return true;
- } else {
- state.init.push(update);
- return false;
+ if (is_dom_property(name)) {
+ return b.assignment('=', b.member(node_id, name), value);
}
+
+ return b.call(
+ name.startsWith('xlink') ? '$.set_xlink_attribute' : '$.set_attribute',
+ node_id,
+ b.literal(name),
+ value,
+ is_ignored(element, 'hydration_attribute_changed') && b.true
+ );
}
/**
- * Like `build_element_attribute_update_assignment` but without any special attribute treatment.
+ * Like `build_element_attribute_update` but without any special attribute treatment.
* @param {Identifier} node_id
* @param {AST.Attribute} attribute
* @param {ComponentContext} context
- * @returns {boolean}
*/
function build_custom_element_attribute_update_assignment(node_id, attribute, context) {
- const state = context.state;
- const name = attribute.name; // don't lowercase, as we set the element's property, which might be case sensitive
- let { has_call, value } = build_attribute_value(attribute.value, context);
+ const { value, has_state } = build_attribute_value(attribute.value, context);
- // We assume that noone's going to redefine the semantics of the class attribute on custom elements, i.e. it's still used for CSS classes
- if (name === 'class' && attribute.metadata.needs_clsx) {
- if (context.state.analysis.css.hash) {
- value = b.array([value, b.literal(context.state.analysis.css.hash)]);
- }
- value = b.call('$.clsx', value);
- }
+ // don't lowercase name, as we set the element's property, which might be case sensitive
+ const call = b.call('$.set_custom_element_data', node_id, b.literal(attribute.name), value);
- const update = b.stmt(b.call('$.set_custom_element_data', node_id, b.literal(name), value));
+ // this is different from other updates — it doesn't get grouped,
+ // because set_custom_element_data may not be idempotent
+ const update = has_state ? b.call('$.template_effect', b.thunk(call)) : call;
- if (attribute.metadata.expression.has_state) {
- if (has_call) {
- state.init.push(build_update(update));
- } else {
- state.update.push(update);
- }
- return true;
- } else {
- state.init.push(update);
- return false;
- }
+ context.state.init.push(b.stmt(update));
}
/**
@@ -681,27 +668,32 @@ function build_custom_element_attribute_update_assignment(node_id, attribute, co
* @param {Identifier} node_id
* @param {AST.Attribute} attribute
* @param {ComponentContext} context
- * @returns {boolean}
*/
function build_element_special_value_attribute(element, node_id, attribute, context) {
const state = context.state;
- const { value } = build_attribute_value(attribute.value, context);
+ const is_select_with_value =
+ // attribute.metadata.dynamic would give false negatives because even if the value does not change,
+ // the inner options could still change, so we need to always treat it as reactive
+ element === 'select' && attribute.value !== true && !is_text_attribute(attribute);
+
+ const { value, has_state } = build_attribute_value(attribute.value, context, (value, metadata) =>
+ metadata.has_call
+ ? // if is a select with value we will also invoke `init_select` which need a reference before the template effect so we memoize separately
+ is_select_with_value
+ ? memoize_expression(state, value)
+ : get_expression_id(state, value)
+ : value
+ );
+
+ const evaluated = context.state.scope.evaluate(value);
+ const assignment = b.assignment('=', b.member(node_id, '__value'), value);
const inner_assignment = b.assignment(
'=',
b.member(node_id, 'value'),
- b.conditional(
- b.binary('==', b.literal(null), b.assignment('=', b.member(node_id, '__value'), value)),
- b.literal(''), // render null/undefined values as empty string to support placeholder options
- value
- )
+ evaluated.is_defined ? assignment : b.logical('??', assignment, b.literal(''))
);
- const is_select_with_value =
- // attribute.metadata.dynamic would give false negatives because even if the value does not change,
- // the inner options could still change, so we need to always treat it as reactive
- element === 'select' && attribute.value !== true && !is_text_attribute(attribute);
-
const update = b.stmt(
is_select_with_value
? b.sequence([
@@ -719,7 +711,7 @@ function build_element_special_value_attribute(element, node_id, attribute, cont
state.init.push(b.stmt(b.call('$.init_select', node_id, b.thunk(value))));
}
- if (attribute.metadata.expression.has_state) {
+ if (has_state) {
const id = state.scope.generate(`${node_id.name}_value`);
build_update_assignment(
state,
@@ -731,9 +723,7 @@ function build_element_special_value_attribute(element, node_id, attribute, cont
value,
update
);
- return true;
} else {
state.init.push(update);
- return false;
}
}
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..6067c2562a 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
@@ -2,7 +2,7 @@
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */
import { unwrap_optional } from '../../../../utils/ast.js';
-import * as b from '../../../../utils/builders.js';
+import * as b from '#compiler/builders';
/**
* @param {AST.RenderTag} node
@@ -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 86734a07ab..ba9fcc7377 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
@@ -1,8 +1,9 @@
/** @import { BlockStatement, Expression, ExpressionStatement, Literal, Property } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */
-import * as b from '../../../../utils/builders.js';
+import * as b from '#compiler/builders';
import { build_attribute_value } from './shared/element.js';
+import { memoize_expression } from './shared/utils.js';
/**
* @param {AST.SlotElement} node
@@ -29,13 +30,17 @@ 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 } = build_attribute_value(attribute.value, context);
+ 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') {
name = /** @type {Literal} */ (value);
is_default = false;
} else if (attribute.name !== 'slot') {
- if (attribute.metadata.expression.has_state) {
+ if (has_state) {
props.push(b.get(attribute.name, [b.return(value)]));
} else {
props.push(b.init(attribute.name, value));
@@ -54,7 +59,7 @@ export function SlotElement(node, context) {
const fallback =
node.fragment.nodes.length === 0
- ? b.literal(null)
+ ? b.null
: b.arrow([b.id('$$anchor')], /** @type {BlockStatement} */ (context.visit(node.fragment)));
const slot = b.call(
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SnippetBlock.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SnippetBlock.js
index 7a0d6981b5..a82645cd7a 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SnippetBlock.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SnippetBlock.js
@@ -1,9 +1,9 @@
-/** @import { BlockStatement, Expression, Identifier, Pattern, Statement } from 'estree' */
+/** @import { AssignmentPattern, BlockStatement, Expression, Identifier, Statement } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */
import { dev } from '../../../../state.js';
import { extract_paths } from '../../../../utils/ast.js';
-import * as b from '../../../../utils/builders.js';
+import * as b from '#compiler/builders';
import { get_value } from './shared/declarations.js';
/**
@@ -12,7 +12,7 @@ import { get_value } from './shared/declarations.js';
*/
export function SnippetBlock(node, context) {
// TODO hoist where possible
- /** @type {Pattern[]} */
+ /** @type {(Identifier | AssignmentPattern)[]} */
const args = [b.id('$$anchor')];
/** @type {BlockStatement} */
@@ -21,6 +21,10 @@ export function SnippetBlock(node, context) {
/** @type {Statement[]} */
const declarations = [];
+ if (dev) {
+ declarations.push(b.stmt(b.call('$.validate_snippet_args', b.spread(b.id('arguments')))));
+ }
+
const transform = { ...context.state.transform };
const child_state = { ...context.state, transform };
@@ -30,12 +34,7 @@ export function SnippetBlock(node, context) {
if (!argument) continue;
if (argument.type === 'Identifier') {
- args.push({
- type: 'AssignmentPattern',
- left: argument,
- right: b.id('$.noop')
- });
-
+ args.push(b.assignment_pattern(argument, b.id('$.noop')));
transform[argument.name] = { read: b.call };
continue;
@@ -72,12 +71,10 @@ export function SnippetBlock(node, context) {
.../** @type {BlockStatement} */ (context.visit(node.body, child_state)).body
]);
- /** @type {Expression} */
- let snippet = b.arrow(args, body);
-
- if (dev) {
- snippet = b.call('$.wrap_snippet', b.id(context.state.analysis.name), snippet);
- }
+ // in dev we use a FunctionExpression (not arrow function) so we can use `arguments`
+ let snippet = dev
+ ? b.call('$.wrap_snippet', b.id(context.state.analysis.name), b.function(null, args, body))
+ : b.arrow(args, body);
const declaration = b.const(node.expression, snippet);
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteBoundary.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteBoundary.js
index ef9f6bd798..b279b5badd 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteBoundary.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteBoundary.js
@@ -1,7 +1,8 @@
/** @import { BlockStatement, Statement, Expression } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */
-import * as b from '../../../../utils/builders.js';
+import { dev } from '../../../../state.js';
+import * as b from '#compiler/builders';
/**
* @param {AST.SvelteBoundary} node
@@ -23,7 +24,7 @@ export function SvelteBoundary(node, context) {
const expression = /** @type {Expression} */ (context.visit(chunk.expression, context.state));
- if (attribute.metadata.expression.has_state) {
+ if (chunk.metadata.expression.has_state) {
props.properties.push(b.get(attribute.name, [b.return(expression)]));
} else {
props.properties.push(b.init(attribute.name, expression));
@@ -33,29 +34,62 @@ export function SvelteBoundary(node, context) {
const nodes = [];
/** @type {Statement[]} */
- const snippet_statements = [];
+ const external_statements = [];
+
+ /** @type {Statement[]} */
+ const internal_statements = [];
+
+ const snippets_visits = [];
// Capture the `failed` implicit snippet prop
for (const child of node.fragment.nodes) {
if (child.type === 'SnippetBlock' && child.expression.name === 'failed') {
+ // we need to delay the visit of the snippets in case they access a ConstTag that is declared
+ // after the snippets so that the visitor for the const tag can be updated
+ snippets_visits.push(() => {
+ /** @type {Statement[]} */
+ const init = [];
+ context.visit(child, { ...context.state, init });
+ props.properties.push(b.prop('init', child.expression, child.expression));
+ external_statements.push(...init);
+ });
+ } else if (child.type === 'ConstTag') {
/** @type {Statement[]} */
const init = [];
context.visit(child, { ...context.state, init });
- props.properties.push(b.prop('init', child.expression, child.expression));
- snippet_statements.push(...init);
+
+ if (dev) {
+ // In dev we must separate the declarations from the code
+ // that eagerly evaluate the expression...
+ for (const statement of init) {
+ if (statement.type === 'VariableDeclaration') {
+ external_statements.push(statement);
+ } else {
+ internal_statements.push(statement);
+ }
+ }
+ } else {
+ external_statements.push(...init);
+ }
} else {
nodes.push(child);
}
}
+ snippets_visits.forEach((visit) => visit());
+
const block = /** @type {BlockStatement} */ (context.visit({ ...node.fragment, nodes }));
+ if (dev && internal_statements.length) {
+ block.body.unshift(...internal_statements);
+ }
+
const boundary = b.stmt(
b.call('$.boundary', context.state.node, props, b.arrow([b.id('$$anchor')], block))
);
context.state.template.push('');
context.state.init.push(
- snippet_statements.length > 0 ? b.block([...snippet_statements, boundary]) : boundary
+ external_statements.length > 0 ? b.block([...external_statements, boundary]) : boundary
);
}
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteElement.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteElement.js
index ba66fe29d6..ee597dd043 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteElement.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteElement.js
@@ -1,21 +1,12 @@
-/** @import { BlockStatement, Expression, ExpressionStatement, Identifier, ObjectExpression, Statement } from 'estree' */
+/** @import { BlockStatement, Expression, ExpressionStatement, Statement } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */
-import { dev, is_ignored, locator } from '../../../../state.js';
-import {
- get_attribute_expression,
- is_event_attribute,
- is_text_attribute
-} from '../../../../utils/ast.js';
-import * as b from '../../../../utils/builders.js';
+import { dev, locator } from '../../../../state.js';
+import { is_text_attribute } from '../../../../utils/ast.js';
+import * as b from '#compiler/builders';
import { determine_namespace_for_children } from '../../utils.js';
-import {
- build_attribute_value,
- build_class_directives,
- build_set_attributes,
- build_style_directives
-} from './shared/element.js';
-import { build_render_statement, build_update } from './shared/utils.js';
+import { build_attribute_value, build_set_attributes, build_set_class } from './shared/element.js';
+import { build_render_statement } from './shared/utils.js';
/**
* @param {AST.SvelteElement} node
@@ -49,9 +40,9 @@ export function SvelteElement(node, context) {
state: {
...context.state,
node: element_id,
- before_init: [],
init: [],
update: [],
+ expressions: [],
after_update: []
}
};
@@ -81,36 +72,29 @@ export function SvelteElement(node, context) {
// Let bindings first, they can be used on attributes
context.state.init.push(...lets); // create computeds in the outer context; the dynamic element is the single child of this slot
- // Then do attributes
- let is_attributes_reactive = false;
-
- if (attributes.length === 0) {
- if (context.state.analysis.css.hash) {
- inner_context.state.init.push(
- b.stmt(b.call('$.set_class', element_id, b.literal(context.state.analysis.css.hash)))
- );
- }
- } else {
+ if (
+ attributes.length === 1 &&
+ attributes[0].type === 'Attribute' &&
+ attributes[0].name.toLowerCase() === 'class' &&
+ is_text_attribute(attributes[0])
+ ) {
+ build_set_class(node, element_id, attributes[0], class_directives, inner_context, false);
+ } else if (attributes.length) {
const attributes_id = b.id(context.state.scope.generate('attributes'));
// Always use spread because we don't know whether the element is a custom element or not,
// therefore we need to do the "how to set an attribute" logic at runtime.
- is_attributes_reactive = build_set_attributes(
+ build_set_attributes(
attributes,
+ class_directives,
+ style_directives,
inner_context,
node,
element_id,
- attributes_id,
- b.binary('===', b.member(element_id, 'namespaceURI'), b.id('$.NAMESPACE_SVG')),
- b.call(b.member(b.member(element_id, 'nodeName'), 'includes'), b.literal('-')),
- context.state
+ attributes_id
);
}
- // class/style directives must be applied last since they could override class/style attributes
- build_class_directives(class_directives, element_id, inner_context, is_attributes_reactive);
- build_style_directives(style_directives, element_id, inner_context, is_attributes_reactive);
-
const get_tag = b.thunk(/** @type {Expression} */ (context.visit(node.tag)));
if (dev) {
@@ -123,7 +107,7 @@ export function SvelteElement(node, context) {
/** @type {Statement[]} */
const inner = inner_context.state.init;
if (inner_context.state.update.length > 0) {
- inner.push(build_render_statement(inner_context.state.update));
+ inner.push(build_render_statement(inner_context.state));
}
inner.push(...inner_context.state.after_update);
inner.push(
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteHead.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteHead.js
index 25fcff0631..0701c37c48 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteHead.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteHead.js
@@ -1,7 +1,7 @@
/** @import { BlockStatement } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */
-import * as b from '../../../../utils/builders.js';
+import * as b from '#compiler/builders';
/**
* @param {AST.SvelteHead} node
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/TitleElement.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/TitleElement.js
index 72cc57b068..7bfdaf1850 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/TitleElement.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/TitleElement.js
@@ -1,6 +1,6 @@
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */
-import * as b from '../../../../utils/builders.js';
+import * as b from '#compiler/builders';
import { build_template_chunk } from './shared/utils.js';
/**
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/TransitionDirective.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/TransitionDirective.js
index e331f36472..41340c1290 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/TransitionDirective.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/TransitionDirective.js
@@ -2,7 +2,7 @@
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */
import { TRANSITION_GLOBAL, TRANSITION_IN, TRANSITION_OUT } from '../../../../../constants.js';
-import * as b from '../../../../utils/builders.js';
+import * as b from '#compiler/builders';
import { parse_directive_name } from './shared/utils.js';
/**
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/UpdateExpression.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/UpdateExpression.js
index 13c1b4bc51..96be119b84 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/UpdateExpression.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/UpdateExpression.js
@@ -1,8 +1,8 @@
/** @import { AssignmentExpression, Expression, UpdateExpression } from 'estree' */
/** @import { Context } from '../types' */
-import { is_ignored } from '../../../../state.js';
import { object } from '../../../../utils/ast.js';
-import * as b from '../../../../utils/builders.js';
+import * as b from '#compiler/builders';
+import { validate_mutation } from './shared/utils.js';
/**
* @param {UpdateExpression} node
@@ -51,7 +51,5 @@ export function UpdateExpression(node, context) {
);
}
- return is_ignored(node, 'ownership_invalid_mutation')
- ? b.call('$.skip_ownership_validation', b.thunk(update))
- : update;
+ return validate_mutation(node, context, update);
}
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/UseDirective.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/UseDirective.js
index be9eb2d516..b95f2fc3ef 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/UseDirective.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/UseDirective.js
@@ -1,7 +1,7 @@
/** @import { Expression } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */
-import * as b from '../../../../utils/builders.js';
+import * as b from '#compiler/builders';
import { parse_directive_name } from './shared/utils.js';
/**
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 afb90bbec7..84044e4ded 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
@@ -3,7 +3,7 @@
/** @import { ComponentClientTransformState, ComponentContext } from '../types' */
import { dev } from '../../../../state.js';
import { extract_paths } from '../../../../utils/ast.js';
-import * as b from '../../../../utils/builders.js';
+import * as b from '#compiler/builders';
import * as assert from '../../../../utils/assert.js';
import { get_rune } from '../../../scope.js';
import { get_prop_source, is_prop_source, is_state_source, should_proxy } from '../utils.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'];
@@ -111,8 +116,7 @@ export function VariableDeclaration(node, context) {
}
const args = /** @type {CallExpression} */ (init).arguments;
- const value =
- args.length === 0 ? b.id('undefined') : /** @type {Expression} */ (context.visit(args[0]));
+ const value = args.length > 0 ? /** @type {Expression} */ (context.visit(args[0])) : b.void0;
if (rune === '$state' || rune === '$state.raw') {
/**
@@ -295,7 +299,7 @@ function create_state_declarators(declarator, { scope, analysis }, value) {
return [
b.declarator(
declarator.id,
- b.call('$.mutable_state', value, analysis.immutable ? b.true : undefined)
+ b.call('$.mutable_source', value, analysis.immutable ? b.true : undefined)
)
];
}
@@ -310,7 +314,7 @@ function create_state_declarators(declarator, { scope, analysis }, value) {
return b.declarator(
path.node,
binding?.kind === 'state'
- ? b.call('$.mutable_state', value, analysis.immutable ? b.true : undefined)
+ ? b.call('$.mutable_source', value, analysis.immutable ? b.true : undefined)
: value
);
})
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 f509cb41a7..c4071c67fe 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
@@ -3,9 +3,8 @@
/** @import { ComponentContext } from '../../types.js' */
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, validate_binding } from '../shared/utils.js';
+import * as b from '#compiler/builders';
+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';
import { determine_slot } from '../../../../../utils/slot.js';
@@ -132,7 +131,13 @@ export function build_component(node, component_name, context, anchor = context.
} else if (attribute.type === 'Attribute') {
if (attribute.name.startsWith('--')) {
custom_css_props.push(
- b.init(attribute.name, build_attribute_value(attribute.value, context).value)
+ b.init(
+ attribute.name,
+ build_attribute_value(attribute.value, context, (value, metadata) =>
+ // TODO put the derived in the local block
+ metadata.has_call ? memoize_expression(context.state, value) : value
+ ).value
+ )
);
continue;
}
@@ -145,63 +150,54 @@ export function build_component(node, component_name, context, anchor = context.
has_children_prop = true;
}
- const { value } = build_attribute_value(attribute.value, context);
-
- if (attribute.metadata.expression.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));
}
} else if (attribute.type === 'BindDirective') {
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 (
+ dev &&
+ attribute.name !== 'this' &&
+ !is_ignored(node, 'ownership_invalid_binding') &&
+ // bind:x={() => x.y, y => x.y = y} will be handled by the assignment expression binding validation
+ attribute.expression.type !== 'SequenceExpression'
+ ) {
+ const left = object(attribute.expression);
+ const binding = left && context.state.scope.get(left.name);
- if (should_add_owner) {
+ if (binding?.kind === 'bindable_prop' || binding?.kind === 'prop') {
+ context.state.analysis.needs_mutation_validation = true;
binding_initializers.push(
b.stmt(
b.call(
- b.id('$.add_owner_effect'),
- expression.type === 'SequenceExpression'
- ? expression.expressions[0]
- : b.thunk(expression),
+ '$$ownership_validator.binding',
+ b.literal(binding.node.name),
b.id(component_name),
- is_ignored(node, 'ownership_invalid_binding') && b.true
+ b.thunk(expression)
)
)
);
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/declarations.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/declarations.js
index 0bd8c352f6..f6bb26daac 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/declarations.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/declarations.js
@@ -1,7 +1,7 @@
/** @import { Identifier } from 'estree' */
/** @import { ComponentContext, Context } from '../../types' */
import { is_state_source } from '../../utils.js';
-import * as b from '../../../../../utils/builders.js';
+import * as b from '#compiler/builders';
/**
* Turns `foo` into `$.get(foo)`
@@ -24,8 +24,8 @@ export function add_state_transformers(context) {
) {
context.state.transform[name] = {
read: binding.declaration_kind === 'var' ? (node) => b.call('$.safe_get', node) : get_value,
- assign: (node, value) => {
- let call = b.call('$.set', node, value);
+ assign: (node, value, proxy = false) => {
+ let call = b.call('$.set', node, value, proxy && b.true);
if (context.state.scope.get(`$${node.name}`)?.kind === 'store_sub') {
call = b.call('$.store_unsub', call, b.literal(`$${node.name}`), b.id('$$stores'));
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 1b0737e31e..a093a0bf4a 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,41 +1,44 @@
-/** @import { Expression, Identifier, ObjectExpression } from 'estree' */
-/** @import { AST, Namespace } from '#compiler' */
-/** @import { ComponentClientTransformState, ComponentContext } from '../../types' */
+/** @import { ArrayExpression, Expression, Identifier, ObjectExpression } from 'estree' */
+/** @import { AST, ExpressionMetadata } from '#compiler' */
+/** @import { ComponentContext } from '../../types' */
+import { escape_html } from '../../../../../../escaping.js';
import { normalize_attribute } from '../../../../../../utils.js';
import { is_ignored } from '../../../../../state.js';
-import { get_attribute_expression, is_event_attribute } from '../../../../../utils/ast.js';
-import * as b from '../../../../../utils/builders.js';
-import { build_getter, create_derived } from '../../utils.js';
-import { build_template_chunk, build_update } from './utils.js';
+import { is_event_attribute } from '../../../../../utils/ast.js';
+import * as b from '#compiler/builders';
+import { build_class_directives_object, build_style_directives_object } from '../RegularElement.js';
+import { build_template_chunk, get_expression_id } from './utils.js';
/**
* @param {Array} attributes
+ * @param {AST.ClassDirective[]} class_directives
+ * @param {AST.StyleDirective[]} style_directives
* @param {ComponentContext} context
* @param {AST.RegularElement | AST.SvelteElement} element
* @param {Identifier} element_id
* @param {Identifier} attributes_id
- * @param {false | Expression} preserve_attribute_case
- * @param {false | Expression} is_custom_element
- * @param {ComponentClientTransformState} state
*/
export function build_set_attributes(
attributes,
+ class_directives,
+ style_directives,
context,
element,
element_id,
- attributes_id,
- preserve_attribute_case,
- is_custom_element,
- state
+ attributes_id
) {
- let has_state = false;
+ let is_dynamic = false;
/** @type {ObjectExpression['properties']} */
const values = [];
for (const attribute of attributes) {
if (attribute.type === 'Attribute') {
- const { value } = build_attribute_value(attribute.value, context);
+ const { value, has_state } = build_attribute_value(
+ attribute.value,
+ context,
+ (value, metadata) => (metadata.has_call ? get_expression_id(context.state, value) : value)
+ );
if (
is_event_attribute(attribute) &&
@@ -49,168 +52,220 @@ export function build_set_attributes(
values.push(b.init(attribute.name, value));
}
- has_state ||= attribute.metadata.expression.has_state;
+ is_dynamic ||= has_state;
} else {
// objects could contain reactive getters -> play it safe and always assume spread attributes are reactive
- has_state = true;
+ is_dynamic = true;
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));
}
}
+ if (class_directives.length) {
+ values.push(
+ b.prop(
+ 'init',
+ b.array([b.id('$.CLASS')]),
+ build_class_directives_object(class_directives, context)
+ )
+ );
+
+ is_dynamic ||=
+ class_directives.find((directive) => directive.metadata.expression.has_state) !== null;
+ }
+
+ if (style_directives.length) {
+ values.push(
+ b.prop(
+ 'init',
+ b.array([b.id('$.STYLE')]),
+ build_style_directives_object(style_directives, context)
+ )
+ );
+
+ is_dynamic ||= style_directives.some((directive) => directive.metadata.expression.has_state);
+ }
+
const call = b.call(
'$.set_attributes',
element_id,
- has_state ? attributes_id : b.literal(null),
+ is_dynamic ? attributes_id : b.null,
b.object(values),
- context.state.analysis.css.hash !== '' && b.literal(context.state.analysis.css.hash),
- preserve_attribute_case,
- is_custom_element,
+ element.metadata.scoped &&
+ context.state.analysis.css.hash !== '' &&
+ b.literal(context.state.analysis.css.hash),
is_ignored(element, 'hydration_attribute_changed') && b.true
);
- if (has_state) {
+ if (is_dynamic) {
context.state.init.push(b.let(attributes_id));
const update = b.stmt(b.assignment('=', attributes_id, call));
context.state.update.push(update);
- return true;
+ } else {
+ context.state.init.push(b.stmt(call));
}
-
- context.state.init.push(b.stmt(call));
- return false;
}
/**
- * Serializes each style directive into something like `$.set_style(element, style_property, value)`
- * and adds it either to init or update, depending on whether or not the value or the attributes are dynamic.
- * @param {AST.StyleDirective[]} style_directives
- * @param {Identifier} element_id
+ * @param {AST.Attribute['value']} value
* @param {ComponentContext} context
- * @param {boolean} is_attributes_reactive
+ * @param {(value: Expression, metadata: ExpressionMetadata) => Expression} memoize
+ * @returns {{ value: Expression, has_state: boolean }}
*/
-export function build_style_directives(
- style_directives,
- element_id,
- context,
- is_attributes_reactive
-) {
- const state = context.state;
+export function build_attribute_value(value, context, memoize = (value) => value) {
+ if (value === true) {
+ return { value: b.true, has_state: false };
+ }
- for (const directive of style_directives) {
- const { has_state, has_call } = directive.metadata.expression;
+ if (!Array.isArray(value) || value.length === 1) {
+ const chunk = Array.isArray(value) ? value[0] : value;
- let value =
- directive.value === true
- ? build_getter({ name: directive.name, type: 'Identifier' }, context.state)
- : build_attribute_value(directive.value, context).value;
+ if (chunk.type === 'Text') {
+ return { value: b.literal(chunk.data), has_state: false };
+ }
- if (has_call) {
- const id = b.id(state.scope.generate('style_directive'));
+ let expression = /** @type {Expression} */ (context.visit(chunk.expression));
- state.init.push(b.const(id, create_derived(state, b.thunk(value))));
- value = b.call('$.get', id);
- }
+ return {
+ value: memoize(expression, chunk.metadata.expression),
+ has_state: chunk.metadata.expression.has_state
+ };
+ }
- const update = b.stmt(
- b.call(
- '$.set_style',
- element_id,
- b.literal(directive.name),
- value,
- /** @type {Expression} */ (directive.modifiers.includes('important') ? b.true : undefined)
- )
- );
+ return build_template_chunk(value, context.visit, context.state, memoize);
+}
- if (!is_attributes_reactive && has_call) {
- state.init.push(build_update(update));
- } else if (is_attributes_reactive || has_state || has_call) {
- state.update.push(update);
- } else {
- state.init.push(update);
- }
+/**
+ * @param {AST.RegularElement | AST.SvelteElement} element
+ * @param {AST.Attribute} attribute
+ */
+export function get_attribute_name(element, attribute) {
+ if (!element.metadata.svg && !element.metadata.mathml) {
+ return normalize_attribute(attribute.name);
}
+
+ return attribute.name;
}
/**
- * Serializes each class directive into something like `$.class_toogle(element, class_name, value)`
- * and adds it either to init or update, depending on whether or not the value or the attributes are dynamic.
+ * @param {AST.RegularElement | AST.SvelteElement} element
+ * @param {Identifier} node_id
+ * @param {AST.Attribute} attribute
* @param {AST.ClassDirective[]} class_directives
- * @param {Identifier} element_id
* @param {ComponentContext} context
- * @param {boolean} is_attributes_reactive
+ * @param {boolean} is_html
*/
-export function build_class_directives(
- class_directives,
- element_id,
- context,
- is_attributes_reactive
-) {
- const state = context.state;
- for (const directive of class_directives) {
- const { has_state, has_call } = directive.metadata.expression;
- let value = /** @type {Expression} */ (context.visit(directive.expression));
+export function build_set_class(element, node_id, attribute, class_directives, context, is_html) {
+ let { value, has_state } = build_attribute_value(attribute.value, context, (value, metadata) => {
+ if (attribute.metadata.needs_clsx) {
+ value = b.call('$.clsx', value);
+ }
- if (has_call) {
- const id = b.id(state.scope.generate('class_directive'));
+ return metadata.has_call ? get_expression_id(context.state, value) : value;
+ });
- state.init.push(b.const(id, create_derived(state, b.thunk(value))));
- value = b.call('$.get', id);
- }
+ /** @type {Identifier | undefined} */
+ let previous_id;
- const update = b.stmt(b.call('$.toggle_class', element_id, b.literal(directive.name), value));
+ /** @type {ObjectExpression | Identifier | undefined} */
+ let prev;
- if (!is_attributes_reactive && has_call) {
- state.init.push(build_update(update));
- } else if (is_attributes_reactive || has_state || has_call) {
- state.update.push(update);
+ /** @type {ObjectExpression | Identifier | undefined} */
+ let next;
+
+ if (class_directives.length) {
+ next = build_class_directives_object(class_directives, context);
+ has_state ||= class_directives.some((d) => d.metadata.expression.has_state);
+
+ if (has_state) {
+ previous_id = b.id(context.state.scope.generate('classes'));
+ context.state.init.push(b.declaration('let', [b.declarator(previous_id)]));
+ prev = previous_id;
} else {
- state.init.push(update);
+ prev = b.object([]);
}
}
-}
-/**
- * @param {AST.Attribute['value']} value
- * @param {ComponentContext} context
- * @returns {{ value: Expression, has_state: boolean, has_call: boolean }}
- */
-export function build_attribute_value(value, context) {
- if (value === true) {
- return { has_state: false, has_call: false, value: b.literal(true) };
+ /** @type {Expression | undefined} */
+ let css_hash;
+
+ if (element.metadata.scoped && context.state.analysis.css.hash) {
+ if (value.type === 'Literal' && (value.value === '' || value.value === null)) {
+ value = b.literal(context.state.analysis.css.hash);
+ } else if (value.type === 'Literal' && typeof value.value === 'string') {
+ value = b.literal(escape_html(value.value, true) + ' ' + context.state.analysis.css.hash);
+ } else {
+ css_hash = b.literal(context.state.analysis.css.hash);
+ }
}
- if (!Array.isArray(value) || value.length === 1) {
- const chunk = Array.isArray(value) ? value[0] : value;
+ if (!css_hash && next) {
+ css_hash = b.null;
+ }
- if (chunk.type === 'Text') {
- return { has_state: false, has_call: false, value: b.literal(chunk.data) };
- }
+ /** @type {Expression} */
+ let set_class = b.call(
+ '$.set_class',
+ node_id,
+ is_html ? b.literal(1) : b.literal(0),
+ value,
+ css_hash,
+ prev,
+ next
+ );
- return {
- has_state: chunk.metadata.expression.has_state,
- has_call: chunk.metadata.expression.has_call,
- value: /** @type {Expression} */ (context.visit(chunk.expression))
- };
+ if (previous_id) {
+ set_class = b.assignment('=', previous_id, set_class);
}
- return build_template_chunk(value, context.visit, context.state);
+ (has_state ? context.state.update : context.state.init).push(b.stmt(set_class));
}
/**
- * @param {AST.RegularElement | AST.SvelteElement} element
+ * @param {Identifier} node_id
* @param {AST.Attribute} attribute
+ * @param {AST.StyleDirective[]} style_directives
+ * @param {ComponentContext} context
*/
-export function get_attribute_name(element, attribute) {
- if (!element.metadata.svg && !element.metadata.mathml) {
- return normalize_attribute(attribute.name);
+export function build_set_style(node_id, attribute, style_directives, context) {
+ let { value, has_state } = build_attribute_value(attribute.value, context, (value, metadata) =>
+ metadata.has_call ? get_expression_id(context.state, value) : value
+ );
+
+ /** @type {Identifier | undefined} */
+ let previous_id;
+
+ /** @type {ObjectExpression | Identifier | undefined} */
+ let prev;
+
+ /** @type {ArrayExpression | ObjectExpression | undefined} */
+ let next;
+
+ if (style_directives.length) {
+ next = build_style_directives_object(style_directives, context);
+ has_state ||= style_directives.some((d) => d.metadata.expression.has_state);
+
+ if (has_state) {
+ previous_id = b.id(context.state.scope.generate('styles'));
+ context.state.init.push(b.declaration('let', [b.declarator(previous_id)]));
+ prev = previous_id;
+ } else {
+ prev = b.object([]);
+ }
}
- return attribute.name;
+ /** @type {Expression} */
+ let set_style = b.call('$.set_style', node_id, value, prev, next);
+
+ if (previous_id) {
+ set_style = b.assignment('=', previous_id, set_style);
+ }
+
+ (has_state ? context.state.update : context.state.init).push(b.stmt(set_style));
}
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/events.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/events.js
index f23f7548ec..d252bd5474 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/events.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/events.js
@@ -3,7 +3,7 @@
/** @import { ComponentContext } from '../../types' */
import { is_capture_event, is_passive_event } from '../../../../../../utils.js';
import { dev, locator } from '../../../../../state.js';
-import * as b from '../../../../../utils/builders.js';
+import * as b from '#compiler/builders';
/**
* @param {AST.Attribute} node
@@ -46,8 +46,12 @@ export function visit_event_attribute(node, context) {
// When we hoist a function we assign an array with the function and all
// hoisted closure params.
- const args = [handler, ...hoisted_params];
- delegated_assignment = b.array(args);
+ if (hoisted_params) {
+ const args = [handler, ...hoisted_params];
+ delegated_assignment = b.array(args);
+ } else {
+ delegated_assignment = handler;
+ }
} else {
delegated_assignment = handler;
}
@@ -123,11 +127,19 @@ export function build_event_handler(node, metadata, context) {
}
// function declared in the script
- if (
- handler.type === 'Identifier' &&
- context.state.scope.get(handler.name)?.declaration_kind !== 'import'
- ) {
- return handler;
+ if (handler.type === 'Identifier') {
+ const binding = context.state.scope.get(handler.name);
+
+ if (binding?.is_function()) {
+ return handler;
+ }
+
+ // local variable can be assigned directly
+ // except in dev mode where when need $.apply()
+ // in order to handle warnings.
+ if (!dev && binding?.declaration_kind !== 'import') {
+ return handler;
+ }
}
if (metadata.has_call) {
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 7674fd1eb2..c91e2b3b44 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
@@ -3,8 +3,9 @@
/** @import { ComponentContext } from '../../types' */
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 { build_template_chunk, build_update } from './utils.js';
+import * as b from '#compiler/builders';
+import { is_custom_element_node } from '../../../../nodes.js';
+import { build_template_chunk } from './utils.js';
/**
* Processes an array of template nodes, joining sibling text/expression nodes
@@ -69,7 +70,7 @@ export function process_children(nodes, initial, is_element, { visit, state }) {
state.template.push(' ');
- const { has_state, has_call, value } = build_template_chunk(sequence, visit, state);
+ const { has_state, value } = build_template_chunk(sequence, visit, state);
// if this is a standalone `{expression}`, make sure we handle the case where
// no text node was created because the expression was empty during SSR
@@ -78,9 +79,7 @@ export function process_children(nodes, initial, is_element, { visit, state }) {
const update = b.stmt(b.call('$.set_text', id, value));
- if (has_call && !within_bound_contenteditable) {
- state.init.push(build_update(update));
- } else if (has_state && !within_bound_contenteditable) {
+ if (has_state && !within_bound_contenteditable) {
state.update.push(update);
} else {
state.init.push(b.stmt(b.assignment('=', b.member(id, 'nodeValue'), value)));
@@ -130,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') {
@@ -145,6 +144,17 @@ function is_static_element(node, state) {
return false;
}
+ if (attribute.name === 'dir') {
+ return false;
+ }
+
+ if (
+ ['input', 'textarea'].includes(node.name) &&
+ ['value', 'checked'].includes(attribute.name)
+ ) {
+ return false;
+ }
+
if (node.name === 'option' && attribute.name === 'value') {
return false;
}
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/special_element.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/special_element.js
index 558bc4fee7..c878f2fc07 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/special_element.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/special_element.js
@@ -1,7 +1,7 @@
/** @import { Expression } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../../types' */
-import * as b from '../../../../../utils/builders.js';
+import * as b from '#compiler/builders';
/**
*
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 1854baa1e9..380cf6cd02 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,80 +1,109 @@
-/** @import { Expression, ExpressionStatement, Identifier, MemberExpression, SequenceExpression, Statement, Super } from 'estree' */
-/** @import { AST } from '#compiler' */
-/** @import { ComponentClientTransformState } from '../../types' */
+/** @import { AssignmentExpression, Expression, ExpressionStatement, Identifier, MemberExpression, SequenceExpression, Literal, Super, UpdateExpression } from 'estree' */
+/** @import { AST, ExpressionMetadata } from '#compiler' */
+/** @import { ComponentClientTransformState, Context } from '../../types' */
import { walk } from 'zimmerframe';
import { object } from '../../../../../utils/ast.js';
-import * as b from '../../../../../utils/builders.js';
+import * as b from '#compiler/builders';
import { sanitize_template_string } from '../../../../../utils/sanitize_template_string.js';
import { regex_is_valid_identifier } from '../../../../patterns.js';
-import { create_derived } from '../../utils.js';
import is_reference from 'is-reference';
-import { locator } from '../../../../../state.js';
+import { dev, is_ignored, locator } from '../../../../../state.js';
+import { create_derived } from '../../utils.js';
+
+/**
+ * @param {ComponentClientTransformState} state
+ * @param {Expression} value
+ */
+export function memoize_expression(state, value) {
+ const id = b.id(state.scope.generate('expression'));
+ state.init.push(b.const(id, create_derived(state, b.thunk(value))));
+ return b.call('$.get', id);
+}
+
+/**
+ *
+ * @param {ComponentClientTransformState} state
+ * @param {Expression} value
+ */
+export function get_expression_id(state, value) {
+ return b.id(`$${state.expressions.push(value) - 1}`);
+}
/**
* @param {Array} values
* @param {(node: AST.SvelteNode, state: any) => any} visit
* @param {ComponentClientTransformState} state
- * @returns {{ value: Expression, has_state: boolean, has_call: boolean }}
+ * @param {(value: Expression, metadata: ExpressionMetadata) => Expression} memoize
+ * @returns {{ value: Expression, has_state: boolean }}
*/
-export function build_template_chunk(values, visit, state) {
+export function build_template_chunk(
+ values,
+ visit,
+ state,
+ memoize = (value, metadata) => (metadata.has_call ? get_expression_id(state, value) : value)
+) {
/** @type {Expression[]} */
const expressions = [];
let quasi = b.quasi('');
const quasis = [quasi];
- let has_call = false;
let has_state = false;
- let contains_multiple_call_expression = false;
-
- for (const node of values) {
- if (node.type === 'ExpressionTag') {
- const metadata = node.metadata.expression;
-
- contains_multiple_call_expression ||= has_call && metadata.has_call;
- has_call ||= metadata.has_call;
- has_state ||= metadata.has_state;
- }
- }
for (let i = 0; i < values.length; i++) {
const node = values[i];
if (node.type === 'Text') {
quasi.value.cooked += node.data;
- } else if (node.type === 'ExpressionTag' && node.expression.type === 'Literal') {
+ } else if (node.expression.type === 'Literal') {
if (node.expression.value != null) {
quasi.value.cooked += node.expression.value + '';
}
- } else {
- if (contains_multiple_call_expression) {
- const id = b.id(state.scope.generate('stringified_text'));
- state.init.push(
- b.const(
- id,
- create_derived(
- state,
- b.thunk(
- b.logical(
- '??',
- /** @type {Expression} */ (visit(node.expression, state)),
- b.literal('')
- )
- )
- )
- )
- );
- expressions.push(b.call('$.get', id));
- } else if (values.length === 1) {
+ } else if (
+ node.expression.type !== 'Identifier' ||
+ node.expression.name !== 'undefined' ||
+ state.scope.get('undefined')
+ ) {
+ let value = memoize(
+ /** @type {Expression} */ (visit(node.expression, state)),
+ node.metadata.expression
+ );
+
+ has_state ||= node.metadata.expression.has_state;
+
+ 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: visit(node.expression, state), has_state, has_call };
- } else {
- expressions.push(b.logical('??', visit(node.expression, state), b.literal('')));
+ return { value, has_state };
+ }
+
+ 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('') };
+ }
}
- quasi = b.quasi('', i + 1 === values.length);
- quasis.push(quasi);
+ const evaluated = state.scope.evaluate(value);
+
+ if (evaluated.is_known) {
+ quasi.value.cooked += evaluated.value + '';
+ } else {
+ if (!evaluated.is_defined) {
+ // add `?? ''` where necessary
+ value = b.logical('??', value, b.literal(''));
+ }
+
+ expressions.push(value);
+
+ quasi = b.quasi('', i + 1 === values.length);
+ quasis.push(quasi);
+ }
}
}
@@ -82,28 +111,32 @@ export function build_template_chunk(values, visit, state) {
quasi.value.raw = sanitize_template_string(/** @type {string} */ (quasi.value.cooked));
}
- const value = b.template(quasis, expressions);
-
- return { value, has_state, has_call };
-}
-
-/**
- * @param {Statement} statement
- */
-export function build_update(statement) {
- const body =
- statement.type === 'ExpressionStatement' ? statement.expression : b.block([statement]);
+ const value =
+ expressions.length > 0
+ ? b.template(quasis, expressions)
+ : b.literal(/** @type {string} */ (quasi.value.cooked));
- return b.stmt(b.call('$.template_effect', b.thunk(body)));
+ return { value, has_state };
}
/**
- * @param {Statement[]} update
+ * @param {ComponentClientTransformState} state
*/
-export function build_render_statement(update) {
- return update.length === 1
- ? build_update(update[0])
- : b.stmt(b.call('$.template_effect', b.thunk(b.block(update))));
+export function build_render_statement(state) {
+ return b.stmt(
+ b.call(
+ '$.template_effect',
+ b.arrow(
+ state.expressions.map((_, i) => b.id(`$${i}`)),
+ state.update.length === 1 && state.update[0].type === 'ExpressionStatement'
+ ? state.update[0].expression
+ : b.block(state.update)
+ ),
+ state.expressions.length > 0 &&
+ b.array(state.expressions.map((expression) => b.thunk(expression))),
+ state.expressions.length > 0 && !state.analysis.runes && b.id('$.derived_safe_equal')
+ )
+ );
}
/**
@@ -239,12 +272,16 @@ export function validate_binding(state, binding, expression) {
const loc = locator(binding.start);
+ const obj = /** @type {Expression} */ (expression.object);
+
state.init.push(
b.stmt(
b.call(
'$.validate_binding',
b.literal(state.analysis.source.slice(binding.start, binding.end)),
- b.thunk(/** @type {Expression} */ (expression.object)),
+ b.thunk(
+ state.store_to_invalidate ? b.sequence([b.call('$.mark_store_binding'), obj]) : obj
+ ),
b.thunk(
/** @type {Expression} */ (
expression.computed
@@ -258,3 +295,60 @@ export function validate_binding(state, binding, expression) {
)
);
}
+
+/**
+ * In dev mode validate mutations to props
+ * @param {AssignmentExpression | UpdateExpression} node
+ * @param {Context} context
+ * @param {Expression} expression
+ */
+export function validate_mutation(node, context, expression) {
+ let left = /** @type {Expression | Super} */ (
+ node.type === 'AssignmentExpression' ? node.left : node.argument
+ );
+
+ if (!dev || left.type !== 'MemberExpression' || is_ignored(node, 'ownership_invalid_mutation')) {
+ return expression;
+ }
+
+ const name = object(left);
+ if (!name) return expression;
+
+ const binding = context.state.scope.get(name.name);
+ if (binding?.kind !== 'prop' && binding?.kind !== 'bindable_prop') return expression;
+
+ const state = /** @type {ComponentClientTransformState} */ (context.state);
+ state.analysis.needs_mutation_validation = true;
+
+ /** @type {Array} */
+ const path = [];
+
+ while (left.type === 'MemberExpression') {
+ if (left.property.type === 'Literal') {
+ path.unshift(left.property);
+ } else if (left.property.type === 'Identifier') {
+ if (left.computed) {
+ path.unshift(left.property);
+ } else {
+ path.unshift(b.literal(left.property.name));
+ }
+ } else {
+ return expression;
+ }
+
+ left = left.object;
+ }
+
+ path.unshift(b.literal(name.name));
+
+ const loc = locator(/** @type {number} */ (left.start));
+
+ return b.call(
+ '$$ownership_validator.mutation',
+ b.literal(binding.prop_alias),
+ b.array(path),
+ expression,
+ loc && b.literal(loc.line),
+ loc && b.literal(loc.column)
+ );
+}
diff --git a/packages/svelte/src/compiler/phases/3-transform/css/index.js b/packages/svelte/src/compiler/phases/3-transform/css/index.js
index 5b0dcd5588..9f1142cce9 100644
--- a/packages/svelte/src/compiler/phases/3-transform/css/index.js
+++ b/packages/svelte/src/compiler/phases/3-transform/css/index.js
@@ -59,7 +59,8 @@ export function render_stylesheet(source, analysis, options) {
// generateMap takes care of calculating source relative to file
source: options.filename,
file: options.cssOutputFilename || options.filename
- })
+ }),
+ hasGlobal: analysis.css.has_global
};
merge_with_preprocessor_map(css, options, css.map.sources[0]);
@@ -169,7 +170,11 @@ const visitors = {
if (node.metadata.is_global_block) {
const selector = node.prelude.children[0];
- if (selector.children.length === 1 && selector.children[0].selectors.length === 1) {
+ if (
+ node.prelude.children.length === 1 &&
+ selector.children.length === 1 &&
+ selector.children[0].selectors.length === 1
+ ) {
// `:global {...}`
if (state.minify) {
state.code.remove(node.start, node.block.start + 1);
@@ -193,7 +198,7 @@ const visitors = {
SelectorList(node, { state, next, path }) {
// Only add comments if we're not inside a complex selector that itself is unused or a global block
if (
- !is_in_global_block(path) &&
+ (!is_in_global_block(path) || node.children.length > 1) &&
!path.find((n) => n.type === 'ComplexSelector' && !n.metadata.used)
) {
const children = node.children;
@@ -281,13 +286,24 @@ const visitors = {
const global = /** @type {AST.CSS.PseudoClassSelector} */ (relative_selector.selectors[0]);
remove_global_pseudo_class(global, relative_selector.combinator, context.state);
- if (
- node.metadata.rule?.metadata.parent_rule &&
- global.args === null &&
- relative_selector.combinator === null
- ) {
- // div { :global.x { ... } } becomes div { &.x { ... } }
- context.state.code.prependRight(global.start, '&');
+ const parent_rule = node.metadata.rule?.metadata.parent_rule;
+ if (parent_rule && global.args === null) {
+ if (relative_selector.combinator === null) {
+ // div { :global.x { ... } } becomes div { &.x { ... } }
+ context.state.code.prependRight(global.start, '&');
+ }
+
+ // In case of multiple :global selectors in a selector list we gotta delete the comma, too, but only if
+ // the next selector is used; if it's unused then the comma deletion happens as part of removal of that next selector
+ if (
+ parent_rule.prelude.children.length > 1 &&
+ node.children.length === node.children.findIndex((s) => s === relative_selector) - 1
+ ) {
+ const next_selector = parent_rule.prelude.children.find((s) => s.start > global.end);
+ if (next_selector && next_selector.metadata.used) {
+ context.state.code.update(global.end, next_selector.start, '');
+ }
+ }
}
continue;
} else {
@@ -379,7 +395,9 @@ function remove_global_pseudo_class(selector, combinator, state) {
// div :global.x becomes div.x
while (/\s/.test(state.code.original[start - 1])) start--;
}
- state.code.remove(start, selector.start + ':global'.length);
+
+ // update(...), not remove(...) because there could be a closing unused comment at the end
+ state.code.update(start, selector.start + ':global'.length, '');
} else {
state.code
.remove(selector.start, selector.start + ':global('.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..e7896991d9 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
@@ -5,7 +5,7 @@
import { walk } from 'zimmerframe';
import { set_scope } from '../../scope.js';
import { extract_identifiers } from '../../../utils/ast.js';
-import * as b from '../../../utils/builders.js';
+import * as b from '#compiler/builders';
import { dev, filename } from '../../../state.js';
import { render_stylesheet } from '../css/index.js';
import { AssignmentExpression } from './visitors/AssignmentExpression.js';
@@ -186,12 +186,10 @@ export function server_component(analysis, options) {
...snippets,
b.let('$$settled', b.true),
b.let('$$inner_payload'),
- b.stmt(
- b.function(
- b.id('$$render_inner'),
- [b.id('$$payload')],
- b.block(/** @type {Statement[]} */ (rest))
- )
+ b.function_declaration(
+ b.id('$$render_inner'),
+ [b.id('$$payload')],
+ b.block(/** @type {Statement[]} */ (rest))
),
b.do_while(
b.unary('!', b.id('$$settled')),
@@ -244,6 +242,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/AssignmentExpression.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/AssignmentExpression.js
index 6364063b3b..071a12f9bc 100644
--- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/AssignmentExpression.js
+++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/AssignmentExpression.js
@@ -1,7 +1,7 @@
/** @import { AssignmentExpression, AssignmentOperator, Expression, Pattern } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { Context, ServerTransformState } from '../types.js' */
-import * as b from '../../../../utils/builders.js';
+import * as b from '#compiler/builders';
import { build_assignment_value } from '../../../../utils/ast.js';
import { visit_assignment_expression } from '../../shared/assignments.js';
diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/AwaitBlock.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/AwaitBlock.js
index 8fc82b8905..35e431f43d 100644
--- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/AwaitBlock.js
+++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/AwaitBlock.js
@@ -1,8 +1,8 @@
/** @import { BlockStatement, Expression, Pattern } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types.js' */
-import * as b from '../../../../utils/builders.js';
-import { empty_comment } from './shared/utils.js';
+import * as b from '#compiler/builders';
+import { block_close } from './shared/utils.js';
/**
* @param {AST.AwaitBlock} node
@@ -10,10 +10,10 @@ import { empty_comment } from './shared/utils.js';
*/
export function AwaitBlock(node, context) {
context.state.template.push(
- empty_comment,
b.stmt(
b.call(
'$.await',
+ b.id('$$payload'),
/** @type {Expression} */ (context.visit(node.expression)),
b.thunk(
node.pending ? /** @type {BlockStatement} */ (context.visit(node.pending)) : b.block([])
@@ -21,13 +21,9 @@ export function AwaitBlock(node, context) {
b.arrow(
node.value ? [/** @type {Pattern} */ (context.visit(node.value))] : [],
node.then ? /** @type {BlockStatement} */ (context.visit(node.then)) : b.block([])
- ),
- b.arrow(
- node.error ? [/** @type {Pattern} */ (context.visit(node.error))] : [],
- node.catch ? /** @type {BlockStatement} */ (context.visit(node.catch)) : b.block([])
)
)
),
- empty_comment
+ block_close
);
}
diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/CallExpression.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/CallExpression.js
index 386c6b6ff3..5bcbdee9fb 100644
--- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/CallExpression.js
+++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/CallExpression.js
@@ -1,7 +1,7 @@
/** @import { CallExpression, Expression } from 'estree' */
/** @import { Context } from '../types.js' */
import { is_ignored } from '../../../../state.js';
-import * as b from '../../../../utils/builders.js';
+import * as b from '#compiler/builders';
import { get_rune } from '../../../scope.js';
import { transform_inspect_rune } from '../../utils.js';
@@ -13,11 +13,11 @@ export function CallExpression(node, context) {
const rune = get_rune(node, context.state.scope);
if (rune === '$host') {
- return b.id('undefined');
+ return b.void0;
}
if (rune === '$effect.tracking') {
- return b.literal(false);
+ return b.false;
}
if (rune === '$effect.root') {
diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/ClassBody.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/ClassBody.js
index b03e1b5ab5..9505743053 100644
--- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/ClassBody.js
+++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/ClassBody.js
@@ -2,7 +2,7 @@
/** @import { Context } from '../types.js' */
/** @import { StateField } from '../../client/types.js' */
import { dev } from '../../../../state.js';
-import * as b from '../../../../utils/builders.js';
+import * as b from '#compiler/builders';
import { get_rune } from '../../../scope.js';
/**
diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/Component.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/Component.js
index 01bf50fafa..503b380c50 100644
--- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/Component.js
+++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/Component.js
@@ -1,6 +1,6 @@
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types.js' */
-import * as b from '../../../../utils/builders.js';
+import * as b from '#compiler/builders';
import { build_inline_component } from './shared/component.js';
/**
diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/ConstTag.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/ConstTag.js
index 5f6aea8338..a8e4e575cc 100644
--- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/ConstTag.js
+++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/ConstTag.js
@@ -1,7 +1,7 @@
/** @import { Expression, Pattern } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types.js' */
-import * as b from '../../../../utils/builders.js';
+import * as b from '#compiler/builders';
/**
* @param {AST.ConstTag} node
diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/DebugTag.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/DebugTag.js
index 6fe4e0b82b..31b53fd3eb 100644
--- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/DebugTag.js
+++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/DebugTag.js
@@ -1,7 +1,7 @@
/** @import { Expression } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types.js' */
-import * as b from '../../../../utils/builders.js';
+import * as b from '#compiler/builders';
/**
* @param {AST.DebugTag} node
diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/EachBlock.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/EachBlock.js
index 104f1f2405..ac6c9891a7 100644
--- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/EachBlock.js
+++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/EachBlock.js
@@ -1,8 +1,8 @@
-/** @import { BlockStatement, Expression, Pattern, Statement } from 'estree' */
+/** @import { BlockStatement, Expression, Statement } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types.js' */
import { BLOCK_OPEN_ELSE } from '../../../../../internal/server/hydration.js';
-import * as b from '../../../../utils/builders.js';
+import * as b from '#compiler/builders';
import { block_close, block_open } from './shared/utils.js';
/**
diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/ExpressionStatement.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/ExpressionStatement.js
index 00d0dba5da..f77e19aec2 100644
--- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/ExpressionStatement.js
+++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/ExpressionStatement.js
@@ -1,6 +1,6 @@
/** @import { ExpressionStatement } from 'estree' */
/** @import { Context } from '../types.js' */
-import * as b from '../../../../utils/builders.js';
+import * as b from '#compiler/builders';
import { get_rune } from '../../../scope.js';
/**
diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/Fragment.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/Fragment.js
index a293b98e7e..a1d25980c4 100644
--- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/Fragment.js
+++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/Fragment.js
@@ -1,7 +1,7 @@
/** @import { AST } from '#compiler' */
/** @import { ComponentContext, ComponentServerTransformState } from '../types.js' */
import { clean_nodes, infer_namespace } from '../../utils.js';
-import * as b from '../../../../utils/builders.js';
+import * as b from '#compiler/builders';
import { empty_comment, process_children, build_template } from './shared/utils.js';
/**
diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/HtmlTag.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/HtmlTag.js
index 0d551a884a..9e857a9308 100644
--- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/HtmlTag.js
+++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/HtmlTag.js
@@ -1,7 +1,7 @@
/** @import { Expression } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types.js' */
-import * as b from '../../../../utils/builders.js';
+import * as b from '#compiler/builders';
/**
* @param {AST.HtmlTag} node
diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/Identifier.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/Identifier.js
index b8c2699d54..fa887650b3 100644
--- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/Identifier.js
+++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/Identifier.js
@@ -1,7 +1,7 @@
/** @import { Identifier, Node } from 'estree' */
/** @import { Context } from '../types.js' */
import is_reference from 'is-reference';
-import * as b from '../../../../utils/builders.js';
+import * as b from '#compiler/builders';
import { build_getter } from './shared/utils.js';
/**
diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/IfBlock.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/IfBlock.js
index 4df09aa8b9..eb51c941f5 100644
--- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/IfBlock.js
+++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/IfBlock.js
@@ -2,7 +2,7 @@
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types.js' */
import { BLOCK_OPEN_ELSE } from '../../../../../internal/server/hydration.js';
-import * as b from '../../../../utils/builders.js';
+import * as b from '#compiler/builders';
import { block_close, block_open } from './shared/utils.js';
/**
@@ -10,19 +10,29 @@ import { block_close, block_open } from './shared/utils.js';
* @param {ComponentContext} context
*/
export function IfBlock(node, context) {
- const test = /** @type {Expression} */ (context.visit(node.test));
-
const consequent = /** @type {BlockStatement} */ (context.visit(node.consequent));
+ consequent.body.unshift(b.stmt(b.assignment('+=', b.id('$$payload.out'), block_open)));
+ let if_statement = b.if(/** @type {Expression} */ (context.visit(node.test)), consequent);
- const alternate = node.alternate
- ? /** @type {BlockStatement} */ (context.visit(node.alternate))
- : b.block([]);
+ context.state.template.push(if_statement, block_close);
- consequent.body.unshift(b.stmt(b.assignment('+=', b.id('$$payload.out'), block_open)));
+ let index = 1;
+ let alt = node.alternate;
+ while (alt && alt.nodes.length === 1 && alt.nodes[0].type === 'IfBlock' && alt.nodes[0].elseif) {
+ const elseif = alt.nodes[0];
+ const alternate = /** @type {BlockStatement} */ (context.visit(elseif.consequent));
+ alternate.body.unshift(
+ b.stmt(b.assignment('+=', b.id('$$payload.out'), b.literal(``)))
+ );
+ if_statement = if_statement.alternate = b.if(
+ /** @type {Expression} */ (context.visit(elseif.test)),
+ alternate
+ );
+ alt = elseif.alternate;
+ }
- alternate.body.unshift(
+ if_statement.alternate = alt ? /** @type {BlockStatement} */ (context.visit(alt)) : b.block([]);
+ if_statement.alternate.body.unshift(
b.stmt(b.assignment('+=', b.id('$$payload.out'), b.literal(BLOCK_OPEN_ELSE)))
);
-
- context.state.template.push(b.if(test, consequent, alternate), block_close);
}
diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/LabeledStatement.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/LabeledStatement.js
index 2b394e94e3..83c828b839 100644
--- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/LabeledStatement.js
+++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/LabeledStatement.js
@@ -1,6 +1,6 @@
/** @import { ExpressionStatement, LabeledStatement } from 'estree' */
/** @import { Context } from '../types.js' */
-import * as b from '../../../../utils/builders.js';
+import * as b from '#compiler/builders';
/**
* @param {LabeledStatement} node
diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/MemberExpression.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/MemberExpression.js
index 527c8cf6ed..73631395e6 100644
--- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/MemberExpression.js
+++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/MemberExpression.js
@@ -1,6 +1,6 @@
/** @import { MemberExpression } from 'estree' */
/** @import { Context } from '../types.js' */
-import * as b from '../../../../utils/builders.js';
+import * as b from '#compiler/builders';
/**
* @param {MemberExpression} node
diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/PropertyDefinition.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/PropertyDefinition.js
index 04751a19d1..c9225bb8da 100644
--- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/PropertyDefinition.js
+++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/PropertyDefinition.js
@@ -1,6 +1,6 @@
/** @import { Expression, PropertyDefinition } from 'estree' */
/** @import { Context } from '../types.js' */
-import * as b from '../../../../utils/builders.js';
+import * as b from '#compiler/builders';
import { get_rune } from '../../../scope.js';
/**
diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/RegularElement.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/RegularElement.js
index af50695efa..5901cb4c50 100644
--- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/RegularElement.js
+++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/RegularElement.js
@@ -4,7 +4,7 @@
/** @import { Scope } from '../../../scope.js' */
import { is_void } from '../../../../../utils.js';
import { dev, locator } from '../../../../state.js';
-import * as b from '../../../../utils/builders.js';
+import * as b from '#compiler/builders';
import { clean_nodes, determine_namespace_for_children } from '../../utils.js';
import { build_element_attributes } from './shared/element.js';
import { process_children, build_template } from './shared/utils.js';
diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/RenderTag.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/RenderTag.js
index ebf8c3be1c..dd2ede3ba3 100644
--- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/RenderTag.js
+++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/RenderTag.js
@@ -2,7 +2,7 @@
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types.js' */
import { unwrap_optional } from '../../../../utils/ast.js';
-import * as b from '../../../../utils/builders.js';
+import * as b from '#compiler/builders';
import { empty_comment } from './shared/utils.js';
/**
diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/SlotElement.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/SlotElement.js
index 7ece04ae3d..fee7cb6e02 100644
--- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/SlotElement.js
+++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/SlotElement.js
@@ -1,7 +1,7 @@
/** @import { BlockStatement, Expression, Literal, Property } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types.js' */
-import * as b from '../../../../utils/builders.js';
+import * as b from '#compiler/builders';
import { empty_comment, build_attribute_value } from './shared/utils.js';
/**
@@ -38,7 +38,7 @@ export function SlotElement(node, context) {
const fallback =
node.fragment.nodes.length === 0
- ? b.literal(null)
+ ? b.null
: b.thunk(/** @type {BlockStatement} */ (context.visit(node.fragment)));
const slot = b.call(
diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/SnippetBlock.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/SnippetBlock.js
index eb83917927..5118679b34 100644
--- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/SnippetBlock.js
+++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/SnippetBlock.js
@@ -1,25 +1,35 @@
-/** @import { BlockStatement } from 'estree' */
+/** @import { ArrowFunctionExpression, BlockStatement, CallExpression } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types.js' */
-import * as b from '../../../../utils/builders.js';
+import { dev } from '../../../../state.js';
+import * as b from '#compiler/builders';
/**
* @param {AST.SnippetBlock} node
* @param {ComponentContext} context
*/
export function SnippetBlock(node, context) {
- const fn = b.function_declaration(
- node.expression,
- [b.id('$$payload'), ...node.parameters],
- /** @type {BlockStatement} */ (context.visit(node.body))
- );
+ const body = /** @type {BlockStatement} */ (context.visit(node.body));
+
+ if (dev) {
+ body.body.unshift(b.stmt(b.call('$.validate_snippet_args', b.id('$$payload'))));
+ }
+
+ /** @type {ArrowFunctionExpression | CallExpression} */
+ let fn = b.arrow([b.id('$$payload'), ...node.parameters], body);
+
+ if (dev) {
+ fn = b.call('$.prevent_snippet_stringification', fn);
+ }
+
+ const declaration = b.declaration('const', [b.declarator(node.expression, fn)]);
// @ts-expect-error - TODO remove this hack once $$render_inner for legacy bindings is gone
fn.___snippet = true;
if (node.metadata.can_hoist) {
- context.state.hoisted.push(fn);
+ context.state.hoisted.push(declaration);
} else {
- context.state.init.push(fn);
+ context.state.init.push(declaration);
}
}
diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/SvelteBoundary.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/SvelteBoundary.js
index 0d54feee11..734740c1b1 100644
--- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/SvelteBoundary.js
+++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/SvelteBoundary.js
@@ -2,7 +2,7 @@
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */
import { BLOCK_CLOSE, BLOCK_OPEN } from '../../../../../internal/server/hydration.js';
-import * as b from '../../../../utils/builders.js';
+import * as b from '#compiler/builders';
/**
* @param {AST.SvelteBoundary} node
diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/SvelteElement.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/SvelteElement.js
index 9f6faa33c8..fd16219860 100644
--- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/SvelteElement.js
+++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/SvelteElement.js
@@ -3,7 +3,7 @@
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types.js' */
import { dev, locator } from '../../../../state.js';
-import * as b from '../../../../utils/builders.js';
+import * as b from '#compiler/builders';
import { determine_namespace_for_children } from '../../utils.js';
import { build_element_attributes } from './shared/element.js';
import { build_template } from './shared/utils.js';
diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/SvelteHead.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/SvelteHead.js
index 7da7d8355c..7d064ffbf5 100644
--- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/SvelteHead.js
+++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/SvelteHead.js
@@ -1,7 +1,7 @@
/** @import { BlockStatement } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types.js' */
-import * as b from '../../../../utils/builders.js';
+import * as b from '#compiler/builders';
/**
* @param {AST.SvelteHead} node
diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/SvelteSelf.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/SvelteSelf.js
index fbedcff283..bb0ecb21b2 100644
--- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/SvelteSelf.js
+++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/SvelteSelf.js
@@ -1,6 +1,6 @@
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types.js' */
-import * as b from '../../../../utils/builders.js';
+import * as b from '#compiler/builders';
import { build_inline_component } from './shared/component.js';
/**
diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/TitleElement.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/TitleElement.js
index 8fd1973453..c42df4c646 100644
--- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/TitleElement.js
+++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/TitleElement.js
@@ -1,6 +1,6 @@
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types.js' */
-import * as b from '../../../../utils/builders.js';
+import * as b from '#compiler/builders';
import { process_children, build_template } from './shared/utils.js';
/**
diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/UpdateExpression.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/UpdateExpression.js
index ae78e14c13..8a2f874e22 100644
--- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/UpdateExpression.js
+++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/UpdateExpression.js
@@ -1,6 +1,6 @@
/** @import { UpdateExpression } from 'estree' */
/** @import { Context } from '../types.js' */
-import * as b from '../../../../utils/builders.js';
+import * as b from '#compiler/builders';
/**
* @param {UpdateExpression} node
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 a530b52f76..f4856ad630 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
@@ -4,7 +4,7 @@
/** @import { Scope } from '../../../scope.js' */
import { walk } from 'zimmerframe';
import { build_fallback, extract_identifiers, extract_paths } from '../../../../utils/ast.js';
-import * as b from '../../../../utils/builders.js';
+import * as b from '#compiler/builders';
import { get_rune } from '../../../scope.js';
/**
@@ -27,6 +27,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
@@ -43,7 +48,7 @@ export function VariableDeclaration(node, context) {
) {
const right = node.right.arguments.length
? /** @type {Expression} */ (context.visit(node.right.arguments[0]))
- : b.id('undefined');
+ : b.void0;
return b.assignment_pattern(node.left, right);
}
}
@@ -73,8 +78,7 @@ export function VariableDeclaration(node, context) {
}
const args = /** @type {CallExpression} */ (init).arguments;
- const value =
- args.length === 0 ? b.id('undefined') : /** @type {Expression} */ (context.visit(args[0]));
+ const value = args.length > 0 ? /** @type {Expression} */ (context.visit(args[0])) : b.void0;
const is_destructuring =
declarator.id.type === 'ObjectPattern' || declarator.id.type === 'ArrayPattern';
@@ -204,6 +208,10 @@ export function VariableDeclaration(node, context) {
}
}
+ if (declarations.length === 0) {
+ return b.empty;
+ }
+
return {
...node,
declarations
diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/component.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/component.js
index 695161ff9b..9bccf9e05e 100644
--- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/component.js
+++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/component.js
@@ -2,8 +2,9 @@
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../../types.js' */
import { empty_comment, build_attribute_value } from './utils.js';
-import * as b from '../../../../../utils/builders.js';
+import * as b from '#compiler/builders';
import { is_element_node } from '../../../../nodes.js';
+import { dev } from '../../../../../state.js';
/**
* @param {AST.Component | AST.SvelteComponent | AST.SvelteSelf} node
@@ -238,7 +239,13 @@ export function build_inline_component(node, expression, context) {
)
) {
// create `children` prop...
- push_prop(b.prop('init', b.id('children'), slot_fn));
+ push_prop(
+ b.prop(
+ 'init',
+ b.id('children'),
+ dev ? b.call('$.prevent_snippet_stringification', slot_fn) : slot_fn
+ )
+ );
// and `$$slots.default: true` so that `` on the child works
serialized_slots.push(b.init(slot_name, b.true));
diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/element.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/element.js
index d0d800d3cb..b0bcb8fd6f 100644
--- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/element.js
+++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/element.js
@@ -1,11 +1,7 @@
-/** @import { Expression, Literal } from 'estree' */
-/** @import { AST, Namespace } from '#compiler' */
+/** @import { ArrayExpression, Expression, Literal, ObjectExpression } from 'estree' */
+/** @import { AST } from '#compiler' */
/** @import { ComponentContext, ComponentServerTransformState } from '../../types.js' */
-import {
- get_attribute_chunks,
- is_event_attribute,
- is_text_attribute
-} from '../../../../../utils/ast.js';
+import { is_event_attribute, is_text_attribute } from '../../../../../utils/ast.js';
import { binding_properties } from '../../../../bindings.js';
import {
create_attribute,
@@ -13,7 +9,7 @@ import {
is_custom_element_node
} from '../../../../nodes.js';
import { regex_starts_with_newline } from '../../../../patterns.js';
-import * as b from '../../../../../utils/builders.js';
+import * as b from '#compiler/builders';
import {
ELEMENT_IS_NAMESPACED,
ELEMENT_PRESERVE_ATTRIBUTE_CASE
@@ -24,6 +20,7 @@ import {
is_content_editable_binding,
is_load_error_element
} from '../../../../../../utils.js';
+import { escape_html } from '../../../../../../escaping.js';
const WHITESPACE_INSENSITIVE_ATTRIBUTES = ['class', 'style'];
@@ -47,9 +44,6 @@ export function build_element_attributes(node, context) {
let content = null;
let has_spread = false;
- // Use the index to keep the attributes order which is important for spreading
- let class_index = -1;
- let style_index = -1;
let events_to_capture = new Set();
for (const attribute of node.attributes) {
@@ -85,34 +79,21 @@ export function build_element_attributes(node, context) {
// the defaultValue/defaultChecked properties don't exist as attributes
} else if (attribute.name !== 'defaultValue' && attribute.name !== 'defaultChecked') {
if (attribute.name === 'class') {
- class_index = attributes.length;
-
if (attribute.metadata.needs_clsx) {
- const clsx_value = b.call(
- '$.clsx',
- /** @type {AST.ExpressionTag} */ (attribute.value).expression
- );
attributes.push({
...attribute,
value: {
.../** @type {AST.ExpressionTag} */ (attribute.value),
- expression: context.state.analysis.css.hash
- ? b.binary(
- '+',
- b.binary('+', clsx_value, b.literal(' ')),
- b.literal(context.state.analysis.css.hash)
- )
- : clsx_value
+ expression: b.call(
+ '$.clsx',
+ /** @type {AST.ExpressionTag} */ (attribute.value).expression
+ )
}
});
} else {
attributes.push(attribute);
}
} else {
- if (attribute.name === 'style') {
- style_index = attributes.length;
- }
-
attributes.push(attribute);
}
}
@@ -219,40 +200,30 @@ export function build_element_attributes(node, context) {
}
}
- if (class_directives.length > 0 && !has_spread) {
- const class_attribute = build_class_directives(
- class_directives,
- /** @type {AST.Attribute | null} */ (attributes[class_index] ?? null)
- );
- if (class_index === -1) {
- attributes.push(class_attribute);
- }
- }
-
- if (style_directives.length > 0 && !has_spread) {
- build_style_directives(
- style_directives,
- /** @type {AST.Attribute | null} */ (attributes[style_index] ?? null),
- context
- );
- if (style_index > -1) {
- attributes.splice(style_index, 1);
- }
- }
-
if (has_spread) {
build_element_spread_attributes(node, attributes, style_directives, class_directives, context);
} else {
+ const css_hash = node.metadata.scoped ? context.state.analysis.css.hash : null;
+
for (const attribute of /** @type {AST.Attribute[]} */ (attributes)) {
- if (attribute.value === true || is_text_attribute(attribute)) {
- const name = get_attribute_name(node, attribute);
- const literal_value = /** @type {Literal} */ (
+ const name = get_attribute_name(node, attribute);
+ const can_use_literal =
+ (name !== 'class' || class_directives.length === 0) &&
+ (name !== 'style' || style_directives.length === 0);
+
+ if (can_use_literal && (attribute.value === true || is_text_attribute(attribute))) {
+ let literal_value = /** @type {Literal} */ (
build_attribute_value(
attribute.value,
context,
WHITESPACE_INSENSITIVE_ATTRIBUTES.includes(name)
)
).value;
+
+ if (name === 'class' && css_hash) {
+ literal_value = (String(literal_value) + ' ' + css_hash).trim();
+ }
+
if (name !== 'class' || literal_value) {
context.state.template.push(
b.literal(
@@ -264,19 +235,31 @@ export function build_element_attributes(node, context) {
)
);
}
+
continue;
}
- const name = get_attribute_name(node, attribute);
const value = build_attribute_value(
attribute.value,
context,
WHITESPACE_INSENSITIVE_ATTRIBUTES.includes(name)
);
- context.state.template.push(
- b.call('$.attr', b.literal(name), value, is_boolean_attribute(name) && b.true)
- );
+ // pre-escape and inline literal attributes :
+ if (can_use_literal && value.type === 'Literal' && typeof value.value === 'string') {
+ if (name === 'class' && css_hash) {
+ value.value = (value.value + ' ' + css_hash).trim();
+ }
+ context.state.template.push(b.literal(` ${name}="${escape_html(value.value, true)}"`));
+ } else if (name === 'class') {
+ context.state.template.push(build_attr_class(class_directives, value, context, css_hash));
+ } else if (name === 'style') {
+ context.state.template.push(build_attr_style(style_directives, value, context));
+ } else {
+ context.state.template.push(
+ b.call('$.attr', b.literal(name), value, is_boolean_attribute(name) && b.true)
+ );
+ }
}
}
@@ -322,7 +305,7 @@ function build_element_spread_attributes(
let styles;
let flags = 0;
- if (class_directives.length > 0 || context.state.analysis.css.hash) {
+ if (class_directives.length) {
const properties = class_directives.map((directive) =>
b.init(
directive.name,
@@ -331,11 +314,6 @@ function build_element_spread_attributes(
: /** @type {Expression} */ (context.visit(directive.expression))
)
);
-
- if (context.state.analysis.css.hash) {
- properties.unshift(b.init(context.state.analysis.css.hash, b.literal(true)));
- }
-
classes = b.object(properties);
}
@@ -374,83 +352,90 @@ function build_element_spread_attributes(
})
);
- const args = [object, classes, styles, flags ? b.literal(flags) : undefined];
+ const css_hash =
+ element.metadata.scoped && context.state.analysis.css.hash
+ ? b.literal(context.state.analysis.css.hash)
+ : b.null;
+
+ const args = [object, css_hash, classes, styles, flags ? b.literal(flags) : undefined];
context.state.template.push(b.call('$.spread_attributes', ...args));
}
/**
*
* @param {AST.ClassDirective[]} class_directives
- * @param {AST.Attribute | null} class_attribute
- * @returns
+ * @param {Expression} expression
+ * @param {ComponentContext} context
+ * @param {string | null} hash
*/
-function build_class_directives(class_directives, class_attribute) {
- const expressions = class_directives.map((directive) =>
- b.conditional(directive.expression, b.literal(directive.name), b.literal(''))
- );
-
- if (class_attribute === null) {
- class_attribute = create_attribute('class', -1, -1, []);
+function build_attr_class(class_directives, expression, context, hash) {
+ /** @type {ObjectExpression | undefined} */
+ let directives;
+
+ if (class_directives.length) {
+ directives = b.object(
+ class_directives.map((directive) =>
+ b.prop(
+ 'init',
+ b.literal(directive.name),
+ /** @type {Expression} */ (context.visit(directive.expression, context.state))
+ )
+ )
+ );
}
- const chunks = get_attribute_chunks(class_attribute.value);
- const last = chunks.at(-1);
-
- if (last?.type === 'Text') {
- last.data += ' ';
- last.raw += ' ';
- } else if (last) {
- chunks.push({
- type: 'Text',
- start: -1,
- end: -1,
- data: ' ',
- raw: ' '
- });
- }
+ let css_hash;
- chunks.push({
- type: 'ExpressionTag',
- start: -1,
- end: -1,
- expression: b.call(
- b.member(b.call(b.member(b.array(expressions), 'filter'), b.id('Boolean')), b.id('join')),
- b.literal(' ')
- ),
- metadata: {
- expression: create_expression_metadata()
+ if (hash) {
+ if (expression.type === 'Literal' && typeof expression.value === 'string') {
+ expression.value = (expression.value + ' ' + hash).trim();
+ } else {
+ css_hash = b.literal(hash);
}
- });
+ }
- class_attribute.value = chunks;
- return class_attribute;
+ return b.call('$.attr_class', expression, css_hash, directives);
}
/**
+ *
* @param {AST.StyleDirective[]} style_directives
- * @param {AST.Attribute | null} style_attribute
+ * @param {Expression} expression
* @param {ComponentContext} context
*/
-function build_style_directives(style_directives, style_attribute, context) {
- const styles = style_directives.map((directive) => {
- let value =
- directive.value === true
- ? b.id(directive.name)
- : build_attribute_value(directive.value, context, true);
- if (directive.modifiers.includes('important')) {
- value = b.binary('+', value, b.literal(' !important'));
+function build_attr_style(style_directives, expression, context) {
+ /** @type {ArrayExpression | ObjectExpression | undefined} */
+ let directives;
+
+ if (style_directives.length) {
+ let normal_properties = [];
+ let important_properties = [];
+
+ for (const directive of style_directives) {
+ const expression =
+ directive.value === true
+ ? b.id(directive.name)
+ : build_attribute_value(directive.value, context, true);
+
+ let name = directive.name;
+ if (name[0] !== '-' || name[1] !== '-') {
+ name = name.toLowerCase();
+ }
+
+ const property = b.init(directive.name, expression);
+ if (directive.modifiers.includes('important')) {
+ important_properties.push(property);
+ } else {
+ normal_properties.push(property);
+ }
}
- return b.init(directive.name, value);
- });
-
- const arg =
- style_attribute === null
- ? b.object(styles)
- : b.call(
- '$.merge_styles',
- build_attribute_value(style_attribute.value, context, true),
- b.object(styles)
- );
- context.state.template.push(b.call('$.add_styles', arg));
+ if (important_properties.length) {
+ directives = b.array([b.object(normal_properties), b.object(important_properties)]);
+ } else {
+ directives = b.object(normal_properties);
+ }
+ }
+
+ return b.call('$.attr_style', expression, directives);
}
diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/utils.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/utils.js
index 1a4898a3d7..bbc81d55c5 100644
--- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/utils.js
+++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/utils.js
@@ -1,4 +1,4 @@
-/** @import { AssignmentOperator, Expression, Identifier, Node, Statement, TemplateElement } from 'estree' */
+/** @import { AssignmentOperator, Expression, Identifier, Node, Statement } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext, ServerTransformState } from '../../types.js' */
@@ -8,7 +8,7 @@ import {
BLOCK_OPEN,
EMPTY_COMMENT
} from '../../../../../../internal/server/hydration.js';
-import * as b from '../../../../../utils/builders.js';
+import * as b from '#compiler/builders';
import { sanitize_template_string } from '../../../../../utils/sanitize_template_string.js';
import { regex_whitespaces_strict } from '../../../../patterns.js';
@@ -44,15 +44,17 @@ export function process_children(nodes, { visit, state }) {
if (node.type === 'Text' || node.type === 'Comment') {
quasi.value.cooked +=
node.type === 'Comment' ? `` : escape_html(node.data);
- } else if (node.type === 'ExpressionTag' && node.expression.type === 'Literal') {
- if (node.expression.value != null) {
- quasi.value.cooked += escape_html(node.expression.value + '');
- }
} else {
- expressions.push(b.call('$.escape', /** @type {Expression} */ (visit(node.expression))));
+ const evaluated = state.scope.evaluate(node.expression);
+
+ if (evaluated.is_known) {
+ quasi.value.cooked += escape_html((evaluated.value ?? '') + '');
+ } else {
+ expressions.push(b.call('$.escape', /** @type {Expression} */ (visit(node.expression))));
- quasi = b.quasi('', i + 1 === sequence.length);
- quasis.push(quasi);
+ quasi = b.quasi('', i + 1 === sequence.length);
+ quasis.push(quasi);
+ }
}
}
diff --git a/packages/svelte/src/compiler/phases/3-transform/shared/assignments.js b/packages/svelte/src/compiler/phases/3-transform/shared/assignments.js
index e8e02b8f58..3e6bb0c4c6 100644
--- a/packages/svelte/src/compiler/phases/3-transform/shared/assignments.js
+++ b/packages/svelte/src/compiler/phases/3-transform/shared/assignments.js
@@ -2,7 +2,7 @@
/** @import { Context as ClientContext } from '../client/types.js' */
/** @import { Context as ServerContext } from '../server/types.js' */
import { extract_paths, is_expression_async } from '../../../utils/ast.js';
-import * as b from '../../../utils/builders.js';
+import * as b from '#compiler/builders';
/**
* @template {ClientContext | ServerContext} Context
diff --git a/packages/svelte/src/compiler/phases/3-transform/utils.js b/packages/svelte/src/compiler/phases/3-transform/utils.js
index 46872fbfcf..5aa40c8abb 100644
--- a/packages/svelte/src/compiler/phases/3-transform/utils.js
+++ b/packages/svelte/src/compiler/phases/3-transform/utils.js
@@ -8,7 +8,7 @@ import {
regex_starts_with_newline,
regex_starts_with_whitespaces
} from '../patterns.js';
-import * as b from '../../utils/builders.js';
+import * as b from '#compiler/builders';
import * as e from '../../errors.js';
import { walk } from 'zimmerframe';
import { extract_identifiers } from '../../utils/ast.js';
diff --git a/packages/svelte/src/compiler/phases/nodes.js b/packages/svelte/src/compiler/phases/nodes.js
index 5066833feb..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'))
+ );
}
/**
@@ -44,7 +48,6 @@ export function create_attribute(name, start, end, value) {
name,
value,
metadata: {
- expression: create_expression_metadata(),
delegated: null,
needs_clsx: false
}
diff --git a/packages/svelte/src/compiler/phases/scope.js b/packages/svelte/src/compiler/phases/scope.js
index 3536dd6a18..570d5e22d9 100644
--- a/packages/svelte/src/compiler/phases/scope.js
+++ b/packages/svelte/src/compiler/phases/scope.js
@@ -1,10 +1,10 @@
-/** @import { ClassDeclaration, Expression, FunctionDeclaration, Identifier, ImportDeclaration, MemberExpression, Node, Pattern, VariableDeclarator } from 'estree' */
+/** @import { ArrowFunctionExpression, BinaryOperator, ClassDeclaration, Expression, FunctionDeclaration, FunctionExpression, Identifier, ImportDeclaration, MemberExpression, LogicalOperator, Node, Pattern, UnaryOperator, VariableDeclarator } from 'estree' */
/** @import { Context, Visitor } from 'zimmerframe' */
-/** @import { AST, Binding, DeclarationKind } from '#compiler' */
+/** @import { AST, BindingKind, DeclarationKind } from '#compiler' */
import is_reference from 'is-reference';
import { walk } from 'zimmerframe';
import { create_expression_metadata } from './nodes.js';
-import * as b from '../utils/builders.js';
+import * as b from '#compiler/builders';
import * as e from '../errors.js';
import {
extract_identifiers,
@@ -16,6 +16,353 @@ import { is_reserved, is_rune } from '../../utils.js';
import { determine_slot } from '../utils/slot.js';
import { validate_identifier_name } from './2-analyze/visitors/shared/utils.js';
+const UNKNOWN = Symbol('unknown');
+/** Includes `BigInt` */
+const NUMBER = Symbol('number');
+const STRING = Symbol('string');
+
+export class Binding {
+ /** @type {Scope} */
+ scope;
+
+ /** @type {Identifier} */
+ node;
+
+ /** @type {BindingKind} */
+ kind;
+
+ /** @type {DeclarationKind} */
+ declaration_kind;
+
+ /**
+ * What the value was initialized with.
+ * For destructured props such as `let { foo = 'bar' } = $props()` this is `'bar'` and not `$props()`
+ * @type {null | Expression | FunctionDeclaration | ClassDeclaration | ImportDeclaration | AST.EachBlock | AST.SnippetBlock}
+ */
+ initial = null;
+
+ /** @type {Array<{ node: Identifier; path: AST.SvelteNode[] }>} */
+ references = [];
+
+ /**
+ * For `legacy_reactive`: its reactive dependencies
+ * @type {Binding[]}
+ */
+ legacy_dependencies = [];
+
+ /**
+ * Legacy props: the `class` in `{ export klass as class}`. $props(): The `class` in { class: klass } = $props()
+ * @type {string | null}
+ */
+ prop_alias = null;
+
+ /**
+ * Additional metadata, varies per binding type
+ * @type {null | { inside_rest?: boolean }}
+ */
+ metadata = null;
+
+ mutated = false;
+ reassigned = false;
+
+ /**
+ *
+ * @param {Scope} scope
+ * @param {Identifier} node
+ * @param {BindingKind} kind
+ * @param {DeclarationKind} declaration_kind
+ * @param {Binding['initial']} initial
+ */
+ constructor(scope, node, kind, declaration_kind, initial) {
+ this.scope = scope;
+ this.node = node;
+ this.initial = initial;
+ this.kind = kind;
+ this.declaration_kind = declaration_kind;
+ }
+
+ get updated() {
+ return this.mutated || this.reassigned;
+ }
+
+ /**
+ * @returns {this is Binding & { initial: ArrowFunctionExpression | FunctionDeclaration | FunctionExpression }}
+ */
+ is_function() {
+ if (this.updated) {
+ // even if it's reassigned to another function,
+ // we can't use it directly as e.g. an event handler
+ return false;
+ }
+
+ const type = this.initial?.type;
+
+ return (
+ type === 'ArrowFunctionExpression' ||
+ type === 'FunctionExpression' ||
+ type === 'FunctionDeclaration'
+ );
+ }
+}
+
+class Evaluation {
+ /** @type {Set} */
+ values = new Set();
+
+ /**
+ * True if there is exactly one possible value
+ * @readonly
+ * @type {boolean}
+ */
+ is_known = true;
+
+ /**
+ * True if the value is known to not be null/undefined
+ * @readonly
+ * @type {boolean}
+ */
+ is_defined = true;
+
+ /**
+ * True if the value is known to be a string
+ * @readonly
+ * @type {boolean}
+ */
+ is_string = true;
+
+ /**
+ * True if the value is known to be a number
+ * @readonly
+ * @type {boolean}
+ */
+ is_number = true;
+
+ /**
+ * @readonly
+ * @type {any}
+ */
+ value = undefined;
+
+ /**
+ *
+ * @param {Scope} scope
+ * @param {Expression} expression
+ */
+ constructor(scope, expression) {
+ switch (expression.type) {
+ case 'Literal': {
+ this.values.add(expression.value);
+ break;
+ }
+
+ case 'Identifier': {
+ const binding = scope.get(expression.name);
+
+ if (binding) {
+ if (
+ binding.initial?.type === 'CallExpression' &&
+ get_rune(binding.initial, scope) === '$props.id'
+ ) {
+ this.values.add(STRING);
+ break;
+ }
+
+ const is_prop =
+ binding.kind === 'prop' ||
+ binding.kind === 'rest_prop' ||
+ binding.kind === 'bindable_prop';
+
+ if (!binding.updated && binding.initial !== null && !is_prop) {
+ const evaluation = binding.scope.evaluate(/** @type {Expression} */ (binding.initial));
+ for (const value of evaluation.values) {
+ this.values.add(value);
+ }
+ break;
+ }
+
+ // TODO each index is always defined
+ }
+
+ // TODO glean what we can from reassignments
+ // TODO one day, expose props and imports somehow
+
+ this.values.add(UNKNOWN);
+ break;
+ }
+
+ case 'BinaryExpression': {
+ const a = scope.evaluate(/** @type {Expression} */ (expression.left)); // `left` cannot be `PrivateIdentifier` unless operator is `in`
+ const b = scope.evaluate(expression.right);
+
+ if (a.is_known && b.is_known) {
+ this.values.add(binary[expression.operator](a.value, b.value));
+ break;
+ }
+
+ switch (expression.operator) {
+ case '!=':
+ case '!==':
+ case '<':
+ case '<=':
+ case '>':
+ case '>=':
+ case '==':
+ case '===':
+ case 'in':
+ case 'instanceof':
+ this.values.add(true);
+ this.values.add(false);
+ break;
+
+ case '%':
+ case '&':
+ case '*':
+ case '**':
+ case '-':
+ case '/':
+ case '<<':
+ case '>>':
+ case '>>>':
+ case '^':
+ case '|':
+ this.values.add(NUMBER);
+ break;
+
+ case '+':
+ if (a.is_string || b.is_string) {
+ this.values.add(STRING);
+ } else if (a.is_number && b.is_number) {
+ this.values.add(NUMBER);
+ } else {
+ this.values.add(STRING);
+ this.values.add(NUMBER);
+ }
+ break;
+
+ default:
+ this.values.add(UNKNOWN);
+ }
+ break;
+ }
+
+ case 'ConditionalExpression': {
+ const test = scope.evaluate(expression.test);
+ const consequent = scope.evaluate(expression.consequent);
+ const alternate = scope.evaluate(expression.alternate);
+
+ if (test.is_known) {
+ for (const value of (test.value ? consequent : alternate).values) {
+ this.values.add(value);
+ }
+ } else {
+ for (const value of consequent.values) {
+ this.values.add(value);
+ }
+
+ for (const value of alternate.values) {
+ this.values.add(value);
+ }
+ }
+ break;
+ }
+
+ case 'LogicalExpression': {
+ const a = scope.evaluate(expression.left);
+ const b = scope.evaluate(expression.right);
+
+ if (a.is_known) {
+ if (b.is_known) {
+ this.values.add(logical[expression.operator](a.value, b.value));
+ break;
+ }
+
+ if (
+ (expression.operator === '&&' && !a.value) ||
+ (expression.operator === '||' && a.value) ||
+ (expression.operator === '??' && a.value != null)
+ ) {
+ this.values.add(a.value);
+ } else {
+ for (const value of b.values) {
+ this.values.add(value);
+ }
+ }
+
+ break;
+ }
+
+ for (const value of a.values) {
+ this.values.add(value);
+ }
+
+ for (const value of b.values) {
+ this.values.add(value);
+ }
+ break;
+ }
+
+ case 'UnaryExpression': {
+ const argument = scope.evaluate(expression.argument);
+
+ if (argument.is_known) {
+ this.values.add(unary[expression.operator](argument.value));
+ break;
+ }
+
+ switch (expression.operator) {
+ case '!':
+ case 'delete':
+ this.values.add(false);
+ this.values.add(true);
+ break;
+
+ case '+':
+ case '-':
+ case '~':
+ this.values.add(NUMBER);
+ break;
+
+ case 'typeof':
+ this.values.add(STRING);
+ break;
+
+ case 'void':
+ this.values.add(undefined);
+ break;
+
+ default:
+ this.values.add(UNKNOWN);
+ }
+ break;
+ }
+
+ default: {
+ this.values.add(UNKNOWN);
+ }
+ }
+
+ for (const value of this.values) {
+ this.value = value; // saves having special logic for `size === 1`
+
+ if (value !== STRING && typeof value !== 'string') {
+ this.is_string = false;
+ }
+
+ if (value !== NUMBER && typeof value !== 'number') {
+ this.is_number = false;
+ }
+
+ if (value == null || value === UNKNOWN) {
+ this.is_defined = false;
+ }
+ }
+
+ if (this.values.size > 1 || typeof this.value === 'symbol') {
+ this.is_known = false;
+ }
+ }
+}
+
export class Scope {
/** @type {ScopeRoot} */
root;
@@ -96,26 +443,15 @@ export class Scope {
}
if (this.declarations.has(node.name)) {
- // This also errors on var/function types, but that's arguably a good thing
- e.declaration_duplicate(node, node.name);
+ const binding = this.declarations.get(node.name);
+ if (binding && binding.declaration_kind !== 'var' && declaration_kind !== 'var') {
+ // This also errors on function types, but that's arguably a good thing
+ // declaring function twice is also caught by acorn in the parse phase
+ e.declaration_duplicate(node, node.name);
+ }
}
- /** @type {Binding} */
- const binding = {
- node,
- references: [],
- legacy_dependencies: [],
- initial,
- reassigned: false,
- mutated: false,
- updated: false,
- scope: this,
- kind,
- declaration_kind,
- is_called: false,
- prop_alias: null,
- metadata: null
- };
+ const binding = new Binding(this, node, kind, declaration_kind, initial);
validate_identifier_name(binding, this.function_depth);
@@ -206,8 +542,63 @@ export class Scope {
this.root.conflicts.add(node.name);
}
}
+
+ /**
+ * Does partial evaluation to find an exact value or at least the rough type of the expression.
+ * Only call this once scope has been fully generated in a first pass,
+ * else this evaluates on incomplete data and may yield wrong results.
+ * @param {Expression} expression
+ * @param {Set} values
+ */
+ evaluate(expression, values = new Set()) {
+ return new Evaluation(this, expression);
+ }
}
+/** @type {Record any>} */
+const binary = {
+ '!=': (left, right) => left != right,
+ '!==': (left, right) => left !== right,
+ '<': (left, right) => left < right,
+ '<=': (left, right) => left <= right,
+ '>': (left, right) => left > right,
+ '>=': (left, right) => left >= right,
+ '==': (left, right) => left == right,
+ '===': (left, right) => left === right,
+ in: (left, right) => left in right,
+ instanceof: (left, right) => left instanceof right,
+ '%': (left, right) => left % right,
+ '&': (left, right) => left & right,
+ '*': (left, right) => left * right,
+ '**': (left, right) => left ** right,
+ '+': (left, right) => left + right,
+ '-': (left, right) => left - right,
+ '/': (left, right) => left / right,
+ '<<': (left, right) => left << right,
+ '>>': (left, right) => left >> right,
+ '>>>': (left, right) => left >>> right,
+ '^': (left, right) => left ^ right,
+ '|': (left, right) => left | right
+};
+
+/** @type {Record any>} */
+const unary = {
+ '-': (argument) => -argument,
+ '+': (argument) => +argument,
+ '!': (argument) => !argument,
+ '~': (argument) => ~argument,
+ typeof: (argument) => typeof argument,
+ void: () => undefined,
+ delete: () => true
+};
+
+/** @type {Record any>} */
+const logical = {
+ '||': (left, right) => left || right,
+ '&&': (left, right) => left && right,
+ '??': (left, right) => left ?? right
+};
+
export class ScopeRoot {
/** @type {Set} */
conflicts = new Set();
@@ -575,21 +966,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
@@ -701,8 +1081,6 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
const binding = left && scope.get(left.name);
if (binding !== null && left !== binding.node) {
- binding.updated = true;
-
if (left === expression) {
binding.reassigned = true;
} else {
diff --git a/packages/svelte/src/compiler/phases/types.d.ts b/packages/svelte/src/compiler/phases/types.d.ts
index fe32dbba3e..f98cbe1415 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` */
@@ -51,6 +53,7 @@ export interface ComponentAnalysis extends Analysis {
uses_component_bindings: boolean;
uses_render_tags: boolean;
needs_context: boolean;
+ needs_mutation_validation: boolean;
needs_props: boolean;
/** Set to the first event directive (on:x) found on a DOM element in the code */
event_directive_node: AST.OnDirective | null;
@@ -71,6 +74,7 @@ export interface ComponentAnalysis extends Analysis {
ast: AST.CSS.StyleSheet | null;
hash: string;
keyframes: string[];
+ has_global: boolean;
};
source: string;
undefined_exports: Map;
diff --git a/packages/svelte/src/compiler/private.d.ts b/packages/svelte/src/compiler/private.d.ts
new file mode 100644
index 0000000000..7ac1089373
--- /dev/null
+++ b/packages/svelte/src/compiler/private.d.ts
@@ -0,0 +1,2 @@
+export * from './types/index';
+export * from './index';
diff --git a/packages/svelte/src/compiler/types/css.d.ts b/packages/svelte/src/compiler/types/css.d.ts
index 7b2e6ae5f7..154a06ffb1 100644
--- a/packages/svelte/src/compiler/types/css.d.ts
+++ b/packages/svelte/src/compiler/types/css.d.ts
@@ -34,6 +34,10 @@ export namespace _CSS {
metadata: {
parent_rule: null | Rule;
has_local_selectors: boolean;
+ /**
+ * `true` if the rule contains a ComplexSelector whose RelativeSelectors are all global or global-like
+ */
+ has_global_selectors: boolean;
/**
* `true` if the rule contains a `:global` selector, and therefore everything inside should be unscoped
*/
@@ -64,6 +68,7 @@ export namespace _CSS {
/** @internal */
metadata: {
rule: null | Rule;
+ is_global: boolean;
/** True if this selector applies to an element. For global selectors, this is defined in css-analyze, for others in css-prune while scoping */
used: boolean;
};
diff --git a/packages/svelte/src/compiler/types/index.d.ts b/packages/svelte/src/compiler/types/index.d.ts
index b80b717e42..616c346ad3 100644
--- a/packages/svelte/src/compiler/types/index.d.ts
+++ b/packages/svelte/src/compiler/types/index.d.ts
@@ -1,12 +1,5 @@
-import type {
- ClassDeclaration,
- Expression,
- FunctionDeclaration,
- Identifier,
- ImportDeclaration
-} from 'estree';
import type { SourceMap } from 'magic-string';
-import type { Scope } from '../phases/scope.js';
+import type { Binding } from '../phases/scope.js';
import type { AST, Namespace } from './template.js';
import type { ICompileDiagnostic } from '../utils/compile_diagnostic.js';
@@ -25,6 +18,8 @@ export interface CompileResult {
code: string;
/** A source map */
map: SourceMap;
+ /** Whether or not the CSS includes global rules */
+ hasGlobal: boolean;
};
/**
* An array of warning objects that were generated during compilation. Each warning has several properties:
@@ -241,6 +236,20 @@ export type ValidatedCompileOptions = ValidatedModuleCompileOptions &
hmr: CompileOptions['hmr'];
};
+export type BindingKind =
+ | 'normal' // A variable that is not in any way special
+ | 'prop' // A normal prop (possibly reassigned or mutated)
+ | 'bindable_prop' // A prop one can `bind:` to (possibly reassigned or mutated)
+ | 'rest_prop' // A rest prop
+ | 'raw_state' // A state variable
+ | 'state' // A deeply reactive state variable
+ | 'derived' // A derived variable
+ | 'each' // An each block parameter
+ | 'snippet' // A snippet parameter
+ | 'store_sub' // A $store value
+ | 'legacy_reactive' // A `$:` declaration
+ | 'template'; // A binding declared in the template, e.g. in an `await` block or `const` tag
+
export type DeclarationKind =
| 'var'
| 'let'
@@ -251,66 +260,6 @@ export type DeclarationKind =
| 'rest_param'
| 'synthetic';
-export interface Binding {
- node: Identifier;
- /**
- * - `normal`: A variable that is not in any way special
- * - `prop`: A normal prop (possibly reassigned or mutated)
- * - `bindable_prop`: A prop one can `bind:` to (possibly reassigned or mutated)
- * - `rest_prop`: A rest prop
- * - `state`: A state variable
- * - `derived`: A derived variable
- * - `each`: An each block parameter
- * - `snippet`: A snippet parameter
- * - `store_sub`: A $store value
- * - `legacy_reactive`: A `$:` declaration
- * - `template`: A binding declared in the template, e.g. in an `await` block or `const` tag
- */
- kind:
- | 'normal'
- | 'prop'
- | 'bindable_prop'
- | 'rest_prop'
- | 'state'
- | 'raw_state'
- | 'derived'
- | 'each'
- | 'snippet'
- | 'store_sub'
- | 'legacy_reactive'
- | 'template'
- | 'snippet';
- declaration_kind: DeclarationKind;
- /**
- * What the value was initialized with.
- * For destructured props such as `let { foo = 'bar' } = $props()` this is `'bar'` and not `$props()`
- */
- initial:
- | null
- | Expression
- | FunctionDeclaration
- | ClassDeclaration
- | ImportDeclaration
- | AST.EachBlock
- | AST.SnippetBlock;
- is_called: boolean;
- references: { node: Identifier; path: AST.SvelteNode[] }[];
- mutated: boolean;
- reassigned: boolean;
- /** `true` if mutated _or_ reassigned */
- updated: boolean;
- scope: Scope;
- /** For `legacy_reactive`: its reactive dependencies */
- legacy_dependencies: Binding[];
- /** Legacy props: the `class` in `{ export klass as class}`. $props(): The `class` in { class: klass } = $props() */
- prop_alias: string | null;
- /** Additional metadata, varies per binding type */
- metadata: {
- /** `true` if is (inside) a rest parameter */
- inside_rest?: boolean;
- } | null;
-}
-
export interface ExpressionMetadata {
/** All the bindings that are referenced inside this expression */
dependencies: Set;
@@ -322,5 +271,7 @@ export interface ExpressionMetadata {
export * from './template.js';
+export { Binding, Scope } from '../phases/scope.js';
+
// TODO this chain is a bit weird
export { ReactiveStatement } from '../phases/types.js';
diff --git a/packages/svelte/src/compiler/types/template.d.ts b/packages/svelte/src/compiler/types/template.d.ts
index 8be9aed177..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;
/**
@@ -479,7 +477,6 @@ export namespace AST {
value: true | ExpressionTag | Array;
/** @internal */
metadata: {
- expression: ExpressionMetadata;
/** May be set if this is an event attribute */
delegated: null | DelegatedEvent;
/** May be `true` if this is a `class` attribute that needs `clsx` */
diff --git a/packages/svelte/src/compiler/utils/ast.js b/packages/svelte/src/compiler/utils/ast.js
index 0a24d2ff0d..108f4eff64 100644
--- a/packages/svelte/src/compiler/utils/ast.js
+++ b/packages/svelte/src/compiler/utils/ast.js
@@ -1,7 +1,7 @@
/** @import { AST } from '#compiler' */
/** @import * as ESTree from 'estree' */
import { walk } from 'zimmerframe';
-import * as b from '../utils/builders.js';
+import * as b from '#compiler/builders';
/**
* Gets the left-most identifier of a member expression or identifier.
diff --git a/packages/svelte/src/compiler/utils/builders.js b/packages/svelte/src/compiler/utils/builders.js
index ecb595d74d..736738d19f 100644
--- a/packages/svelte/src/compiler/utils/builders.js
+++ b/packages/svelte/src/compiler/utils/builders.js
@@ -154,6 +154,8 @@ export function unary(operator, argument) {
return { type: 'UnaryExpression', argument, operator, prefix: true };
}
+export const void0 = unary('void', literal(0));
+
/**
* @param {ESTree.Expression} test
* @param {ESTree.Expression} consequent
@@ -483,7 +485,7 @@ export function do_while(test, body) {
const true_instance = literal(true);
const false_instance = literal(false);
-const null_instane = literal(null);
+const null_instance = literal(null);
/** @type {ESTree.DebuggerStatement} */
const debugger_builder = {
@@ -645,7 +647,7 @@ export {
return_builder as return,
if_builder as if,
this_instance as this,
- null_instane as null,
+ null_instance as null,
debugger_builder as debugger
};
diff --git a/packages/svelte/src/compiler/warnings.js b/packages/svelte/src/compiler/warnings.js
index a9ea617d3f..e6fc8caba5 100644
--- a/packages/svelte/src/compiler/warnings.js
+++ b/packages/svelte/src/compiler/warnings.js
@@ -641,11 +641,13 @@ export function reactive_declaration_module_script_dependency(node) {
}
/**
- * State referenced in its own scope will never update. Did you mean to reference it inside a closure?
+ * This reference only captures the initial value of `%name%`. Did you mean to reference it inside a %type% instead?
* @param {null | NodeLike} node
+ * @param {string} name
+ * @param {string} type
*/
-export function state_referenced_locally(node) {
- w(node, 'state_referenced_locally', `State referenced in its own scope will never update. Did you mean to reference it inside a closure?\nhttps://svelte.dev/e/state_referenced_locally`);
+export function state_referenced_locally(node, name, type) {
+ w(node, 'state_referenced_locally', `This reference only captures the initial value of \`${name}\`. Did you mean to reference it inside a ${type} instead?\nhttps://svelte.dev/e/state_referenced_locally`);
}
/**
diff --git a/packages/svelte/src/constants.js b/packages/svelte/src/constants.js
index 03fddc5ebd..8861e440fc 100644
--- a/packages/svelte/src/constants.js
+++ b/packages/svelte/src/constants.js
@@ -33,6 +33,7 @@ export const UNINITIALIZED = Symbol();
export const FILENAME = Symbol('filename');
export const HMR = Symbol('hmr');
+export const NAMESPACE_HTML = 'http://www.w3.org/1999/xhtml';
export const NAMESPACE_SVG = 'http://www.w3.org/2000/svg';
export const NAMESPACE_MATHML = 'http://www.w3.org/1998/Math/MathML';
diff --git a/packages/svelte/src/html-tree-validation.js b/packages/svelte/src/html-tree-validation.js
index 98e74b638f..4ec4ecf26f 100644
--- a/packages/svelte/src/html-tree-validation.js
+++ b/packages/svelte/src/html-tree-validation.js
@@ -186,6 +186,8 @@ export function is_tag_valid_with_ancestor(child_tag, ancestors, child_loc, ance
export function is_tag_valid_with_parent(child_tag, parent_tag, child_loc, parent_loc) {
if (child_tag.includes('-') || parent_tag?.includes('-')) return null; // custom elements can be anything
+ if (parent_tag === 'template') return null; // no errors or warning should be thrown in immediate children of template tags
+
const disallowed = disallowed_children[parent_tag];
const child = child_loc ? `\`<${child_tag}>\` (${child_loc})` : `\`<${child_tag}>\``;
diff --git a/packages/svelte/src/index-client.js b/packages/svelte/src/index-client.js
index 587d766233..efd5628ae9 100644
--- a/packages/svelte/src/index-client.js
+++ b/packages/svelte/src/index-client.js
@@ -1,21 +1,58 @@
/** @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 { 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.
- * It must be called during the component's initialisation (but doesn't need to live *inside* the component;
- * it can be called from an external module).
+ * `onMount`, like [`$effect`](https://svelte.dev/docs/svelte/$effect), schedules a function to run as soon as the component has been mounted to the DOM.
+ * Unlike `$effect`, the provided function only runs once.
*
- * If a function is returned _synchronously_ from `onMount`, it will be called when the component is unmounted.
+ * It must be called during the component's initialisation (but doesn't need to live _inside_ the component;
+ * it can be called from an external module). If a function is returned _synchronously_ from `onMount`,
+ * it will be called when the component is unmounted.
*
- * `onMount` does not run inside [server-side components](https://svelte.dev/docs/svelte/svelte-server#render).
+ * `onMount` functions do not run during [server-side rendering](https://svelte.dev/docs/svelte/svelte-server#render).
*
* @template T
* @param {() => NotFunction | Promise> | (() => any)} fn
@@ -77,7 +114,7 @@ function create_custom_event(type, detail, { bubbles = false, cancelable = false
* The event dispatcher can be typed to narrow the allowed event names and the type of the `detail` argument:
* ```ts
* const dispatch = createEventDispatcher<{
- * loaded: never; // does not take a detail argument
+ * loaded: null; // does not take a detail argument
* change: string; // takes a detail argument of type string, which is required
* optional: number | null; // takes an optional detail argument of type number
* }>();
@@ -170,24 +207,8 @@ function init_update_callbacks(context) {
return (l.u ??= { a: [], b: [], m: [] });
}
-/**
- * Synchronously flushes any pending state changes and those that result from it.
- * @param {() => void} [fn]
- * @returns {void}
- */
-export function flushSync(fn) {
- flush_sync(fn);
-}
-
+export { flushSync } from './internal/client/runtime.js';
+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/index.d.ts b/packages/svelte/src/index.d.ts
index 554510542e..38e6086689 100644
--- a/packages/svelte/src/index.d.ts
+++ b/packages/svelte/src/index.d.ts
@@ -19,6 +19,7 @@ export interface ComponentConstructorOptions<
intro?: boolean;
recover?: boolean;
sync?: boolean;
+ idPrefix?: string;
$$inline?: boolean;
}
diff --git a/packages/svelte/src/internal/client/constants.js b/packages/svelte/src/internal/client/constants.js
index a4840ce4eb..7e5196c606 100644
--- a/packages/svelte/src/internal/client/constants.js
+++ b/packages/svelte/src/internal/client/constants.js
@@ -20,8 +20,8 @@ export const LEGACY_DERIVED_PROP = 1 << 17;
export const INSPECT_EFFECT = 1 << 18;
export const HEAD_EFFECT = 1 << 19;
export const EFFECT_HAS_DERIVED = 1 << 20;
+export const EFFECT_IS_UPDATING = 1 << 21;
export const STATE_SYMBOL = Symbol('$state');
-export const STATE_SYMBOL_METADATA = Symbol('$state metadata');
export const LEGACY_PROPS = Symbol('legacy props');
export const LOADING_ATTR_SYMBOL = Symbol('');
diff --git a/packages/svelte/src/internal/client/context.js b/packages/svelte/src/internal/client/context.js
new file mode 100644
index 0000000000..7c7213b7a2
--- /dev/null
+++ b/packages/svelte/src/internal/client/context.js
@@ -0,0 +1,205 @@
+/** @import { ComponentContext } from '#client' */
+
+import { DEV } from 'esm-env';
+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
+} from './runtime.js';
+import { effect, teardown } 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');
+ 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) {
+ var ctx = (component_context = {
+ p: component_context,
+ c: null,
+ d: false,
+ 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)
+ };
+ }
+
+ teardown(() => {
+ /** @type {ComponentContext} */ (ctx).d = true;
+ });
+
+ 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/console-log.js b/packages/svelte/src/internal/client/dev/console-log.js
index a578ecea45..d314359ef6 100644
--- a/packages/svelte/src/internal/client/dev/console-log.js
+++ b/packages/svelte/src/internal/client/dev/console-log.js
@@ -1,4 +1,4 @@
-import { STATE_SYMBOL } from '../constants.js';
+import { STATE_SYMBOL } from '#client/constants';
import { snapshot } from '../../shared/clone.js';
import * as w from '../warnings.js';
import { untrack } from '../runtime.js';
diff --git a/packages/svelte/src/internal/client/dev/debug.js b/packages/svelte/src/internal/client/dev/debug.js
new file mode 100644
index 0000000000..fbde87a2d7
--- /dev/null
+++ b/packages/svelte/src/internal/client/dev/debug.js
@@ -0,0 +1,109 @@
+/** @import { Derived, Effect, Value } from '#client' */
+
+import {
+ BLOCK_EFFECT,
+ BOUNDARY_EFFECT,
+ BRANCH_EFFECT,
+ CLEAN,
+ DERIVED,
+ EFFECT,
+ MAYBE_DIRTY,
+ RENDER_EFFECT,
+ ROOT_EFFECT
+} from '#client/constants';
+
+/**
+ *
+ * @param {Effect} effect
+ */
+export function root(effect) {
+ while (effect.parent !== null) {
+ effect = effect.parent;
+ }
+
+ return effect;
+}
+
+/**
+ *
+ * @param {Effect} effect
+ */
+export function log_effect_tree(effect, depth = 0) {
+ const flags = effect.f;
+
+ let label = '(unknown)';
+
+ if ((flags & ROOT_EFFECT) !== 0) {
+ label = 'root';
+ } else if ((flags & BOUNDARY_EFFECT) !== 0) {
+ label = 'boundary';
+ } else if ((flags & BLOCK_EFFECT) !== 0) {
+ label = 'block';
+ } else if ((flags & BRANCH_EFFECT) !== 0) {
+ label = 'branch';
+ } else if ((flags & RENDER_EFFECT) !== 0) {
+ label = 'render effect';
+ } else if ((flags & EFFECT) !== 0) {
+ label = 'effect';
+ }
+
+ let status =
+ (flags & CLEAN) !== 0 ? 'clean' : (flags & MAYBE_DIRTY) !== 0 ? 'maybe dirty' : 'dirty';
+
+ // eslint-disable-next-line no-console
+ console.group(`%c${label} (${status})`, `font-weight: ${status === 'clean' ? 'normal' : 'bold'}`);
+
+ if (depth === 0) {
+ const callsite = new Error().stack
+ ?.split('\n')[2]
+ .replace(/\s+at (?: \w+\(?)?(.+)\)?/, (m, $1) => $1.replace(/\?[^:]+/, ''));
+
+ // eslint-disable-next-line no-console
+ console.log(callsite);
+ }
+
+ if (effect.deps !== null) {
+ // eslint-disable-next-line no-console
+ console.groupCollapsed('%cdeps', 'font-weight: normal');
+
+ for (const dep of effect.deps) {
+ log_dep(dep);
+ }
+
+ // eslint-disable-next-line no-console
+ console.groupEnd();
+ }
+
+ let child = effect.first;
+ while (child !== null) {
+ log_effect_tree(child, depth + 1);
+ child = child.next;
+ }
+
+ // eslint-disable-next-line no-console
+ console.groupEnd();
+}
+
+/**
+ *
+ * @param {Value} dep
+ */
+function log_dep(dep) {
+ if ((dep.f & DERIVED) !== 0) {
+ const derived = /** @type {Derived} */ (dep);
+
+ // eslint-disable-next-line no-console
+ console.groupCollapsed('%cderived', 'font-weight: normal', derived.v);
+ if (derived.deps) {
+ for (const d of derived.deps) {
+ log_dep(d);
+ }
+ }
+
+ // eslint-disable-next-line no-console
+ console.groupEnd();
+ } else {
+ // eslint-disable-next-line no-console
+ console.log('state', dep.v);
+ }
+}
diff --git a/packages/svelte/src/internal/client/dev/hmr.js b/packages/svelte/src/internal/client/dev/hmr.js
index ee5e08c0b1..27e2643d16 100644
--- a/packages/svelte/src/internal/client/dev/hmr.js
+++ b/packages/svelte/src/internal/client/dev/hmr.js
@@ -1,6 +1,6 @@
/** @import { Source, Effect, TemplateNode } from '#client' */
import { FILENAME, HMR } from '../../../constants.js';
-import { EFFECT_TRANSPARENT } from '../constants.js';
+import { EFFECT_TRANSPARENT } from '#client/constants';
import { hydrate_node, hydrating } from '../dom/hydration.js';
import { block, branch, destroy_effect } from '../reactivity/effects.js';
import { source } from '../reactivity/sources.js';
diff --git a/packages/svelte/src/internal/client/dev/legacy.js b/packages/svelte/src/internal/client/dev/legacy.js
index a1d2fc8823..02428dc824 100644
--- a/packages/svelte/src/internal/client/dev/legacy.js
+++ b/packages/svelte/src/internal/client/dev/legacy.js
@@ -1,7 +1,6 @@
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';
/** @param {Function & { [FILENAME]: string }} target */
export function check_target(target) {
@@ -15,9 +14,7 @@ export function legacy_api() {
/** @param {string} method */
function error(method) {
- // @ts-expect-error
- const parent = get_component()?.[FILENAME] ?? 'Something';
- e.component_api_changed(parent, method, component[FILENAME]);
+ e.component_api_changed(method, component[FILENAME]);
}
return {
diff --git a/packages/svelte/src/internal/client/dev/ownership.js b/packages/svelte/src/internal/client/dev/ownership.js
index d113d9ae90..19d2cdb343 100644
--- a/packages/svelte/src/internal/client/dev/ownership.js
+++ b/packages/svelte/src/internal/client/dev/ownership.js
@@ -1,264 +1,81 @@
-/** @import { ProxyMetadata } from '#client' */
/** @typedef {{ file: string, line: number, column: number }} Location */
-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 { get_prototype_of } from '../../shared/utils.js';
-import * as w from '../warnings.js';
+import { get_descriptor } from '../../shared/utils.js';
+import { LEGACY_PROPS, STATE_SYMBOL } from '#client/constants';
import { FILENAME } from '../../../constants.js';
-
-/** @type {Record>} */
-const boundaries = {};
-
-const chrome_pattern = /at (?:.+ \()?(.+):(\d+):(\d+)\)?$/;
-const firefox_pattern = /@(.+):(\d+):(\d+)$/;
-
-function get_stack() {
- const stack = new Error().stack;
- if (!stack) return null;
-
- const entries = [];
-
- for (const line of stack.split('\n')) {
- let match = chrome_pattern.exec(line) ?? firefox_pattern.exec(line);
-
- if (match) {
- entries.push({
- file: match[1],
- line: +match[2],
- column: +match[3]
- });
- }
- }
-
- return entries;
-}
+import { component_context } from '../context.js';
+import * as w from '../warnings.js';
+import { sanitize_location } from '../../../utils.js';
/**
- * Determines which `.svelte` component is responsible for a given state change
- * @returns {Function | null}
+ * Sets up a validator that
+ * - traverses the path of a prop to find out if it is allowed to be mutated
+ * - checks that the binding chain is not interrupted
+ * @param {Record} props
*/
-export function get_component() {
- // first 4 lines are svelte internals; adjust this number if we change the internal call stack
- const stack = get_stack()?.slice(4);
- if (!stack) return null;
-
- for (let i = 0; i < stack.length; i++) {
- const entry = stack[i];
- const modules = boundaries[entry.file];
- if (!modules) {
- // If the first entry is not a component, that means the modification very likely happened
- // within a .svelte.js file, possibly triggered by a component. Since these files are not part
- // of the bondaries/component context heuristic, we need to bail in this case, else we would
- // have false positives when the .svelte.ts file provides a state creator function, encapsulating
- // the state and its mutations, and is being called from a component other than the one who
- // called the state creator function.
- if (i === 0) return null;
- continue;
- }
-
- for (const module of modules) {
- if (module.end == null) {
- return null;
- }
- if (module.start.line < entry.line && module.end.line > entry.line) {
- return module.component;
+export function create_ownership_validator(props) {
+ const component = component_context?.function;
+ const parent = component_context?.p?.function;
+
+ return {
+ /**
+ * @param {string} prop
+ * @param {any[]} path
+ * @param {any} result
+ * @param {number} line
+ * @param {number} column
+ */
+ mutation: (prop, path, result, line, column) => {
+ const name = path[0];
+ if (is_bound_or_unset(props, name) || !parent) {
+ return result;
}
- }
- }
-
- return null;
-}
-
-export const ADD_OWNER = Symbol('ADD_OWNER');
-/**
- * Together with `mark_module_end`, this function establishes the boundaries of a `.svelte` file,
- * such that subsequent calls to `get_component` can tell us which component is responsible
- * for a given state change
- */
-export function mark_module_start() {
- const start = get_stack()?.[2];
-
- if (start) {
- (boundaries[start.file] ??= []).push({
- start,
- // @ts-expect-error
- end: null,
- // @ts-expect-error we add the component at the end, since HMR will overwrite the function
- component: null
- });
- }
-}
-
-/**
- * @param {Function} component
- */
-export function mark_module_end(component) {
- const end = get_stack()?.[2];
-
- if (end) {
- const boundaries_file = boundaries[end.file];
- const boundary = boundaries_file[boundaries_file.length - 1];
-
- boundary.end = end;
- boundary.component = component;
- }
-}
+ /** @type {any} */
+ let value = props;
-/**
- * @param {any} object
- * @param {any} owner
- * @param {boolean} [global]
- * @param {boolean} [skip_warning]
- */
-export function add_owner(object, owner, global = false, skip_warning = false) {
- if (object && !global) {
- const component = dev_current_component_function;
- const metadata = object[STATE_SYMBOL_METADATA];
- if (metadata && !has_owner(metadata, component)) {
- let original = get_owner(metadata);
-
- if (owner[FILENAME] !== component[FILENAME] && !skip_warning) {
- w.ownership_invalid_binding(component[FILENAME], owner[FILENAME], original[FILENAME]);
+ for (let i = 0; i < path.length - 1; i++) {
+ value = value[path[i]];
+ if (!value?.[STATE_SYMBOL]) {
+ return result;
+ }
}
- }
- }
-
- add_owner_to_object(object, owner, new Set());
-}
-
-/**
- * @param {() => unknown} get_object
- * @param {any} Component
- * @param {boolean} [skip_warning]
- */
-export function add_owner_effect(get_object, Component, skip_warning = false) {
- user_pre_effect(() => {
- add_owner(get_object(), Component, false, skip_warning);
- });
-}
-
-/**
- * @param {ProxyMetadata | null} from
- * @param {ProxyMetadata} to
- */
-export function widen_ownership(from, to) {
- if (to.owners === null) {
- return;
- }
- while (from) {
- if (from.owners === null) {
- to.owners = null;
- break;
- }
-
- for (const owner of from.owners) {
- to.owners.add(owner);
- }
-
- from = from.parent;
- }
-}
-
-/**
- * @param {any} object
- * @param {Function} owner
- * @param {Set} seen
- */
-function add_owner_to_object(object, owner, seen) {
- const metadata = /** @type {ProxyMetadata} */ (object?.[STATE_SYMBOL_METADATA]);
-
- 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);
- }
- } else if (object && typeof object === 'object') {
- if (seen.has(object)) return;
- seen.add(object);
- if (ADD_OWNER in object && object[ADD_OWNER]) {
- // this is a class with state fields. we put this in a render effect
- // so that if state is replaced (e.g. `instance.name = { first, last }`)
- // the new state is also co-owned by the caller of `getContext`
- render_effect(() => {
- object[ADD_OWNER](owner);
- });
- } else {
- var proto = get_prototype_of(object);
-
- if (proto === Object.prototype) {
- // recurse until we find a state proxy
- for (const key in object) {
- add_owner_to_object(object[key], owner, seen);
- }
- } else if (proto === Array.prototype) {
- // recurse until we find a state proxy
- for (let i = 0; i < object.length; i += 1) {
- add_owner_to_object(object[i], owner, seen);
- }
+ const location = sanitize_location(`${component[FILENAME]}:${line}:${column}`);
+
+ w.ownership_invalid_mutation(name, location, prop, parent[FILENAME]);
+
+ return result;
+ },
+ /**
+ * @param {any} key
+ * @param {any} child_component
+ * @param {() => any} value
+ */
+ binding: (key, child_component, value) => {
+ if (!is_bound_or_unset(props, key) && parent && value()?.[STATE_SYMBOL]) {
+ w.ownership_invalid_binding(
+ component[FILENAME],
+ key,
+ child_component[FILENAME],
+ parent[FILENAME]
+ );
}
}
- }
-}
-
-/**
- * @param {ProxyMetadata} metadata
- * @param {Function} component
- * @returns {boolean}
- */
-function has_owner(metadata, component) {
- if (metadata.owners === null) {
- return true;
- }
-
- return (
- metadata.owners.has(component) ||
- (metadata.parent !== null && has_owner(metadata.parent, component))
- );
+ };
}
/**
- * @param {ProxyMetadata} metadata
- * @returns {any}
+ * @param {Record} props
+ * @param {string} prop_name
*/
-function get_owner(metadata) {
+function is_bound_or_unset(props, prop_name) {
+ // Can be the case when someone does `mount(Component, props)` with `let props = $state({...})`
+ // or `createClassComponent(Component, props)`
+ const is_entry_props = STATE_SYMBOL in props || LEGACY_PROPS in props;
return (
- metadata?.owners?.values().next().value ??
- get_owner(/** @type {ProxyMetadata} */ (metadata.parent))
+ !!get_descriptor(props, prop_name)?.set ||
+ (is_entry_props && prop_name in props) ||
+ !(prop_name in props)
);
}
-
-let skip = false;
-
-/**
- * @param {() => any} fn
- */
-export function skip_ownership_validation(fn) {
- skip = true;
- fn();
- skip = false;
-}
-
-/**
- * @param {ProxyMetadata} metadata
- */
-export function check_ownership(metadata) {
- if (skip) return;
-
- const component = get_component();
-
- if (component && !has_owner(metadata, component)) {
- let original = get_owner(metadata);
-
- // @ts-expect-error
- if (original[FILENAME] !== component[FILENAME]) {
- // @ts-expect-error
- w.ownership_invalid_mutation(component[FILENAME], original[FILENAME]);
- } else {
- w.ownership_invalid_mutation();
- }
- }
-}
diff --git a/packages/svelte/src/internal/client/dev/tracing.js b/packages/svelte/src/internal/client/dev/tracing.js
index 3881ef3442..ad80e75c3d 100644
--- a/packages/svelte/src/internal/client/dev/tracing.js
+++ b/packages/svelte/src/internal/client/dev/tracing.js
@@ -1,8 +1,8 @@
-/** @import { Derived, Reaction, Signal, Value } from '#client' */
+/** @import { Derived, Reaction, Value } from '#client' */
import { UNINITIALIZED } from '../../../constants.js';
import { snapshot } from '../../shared/clone.js';
import { define_property } from '../../shared/utils.js';
-import { DERIVED, STATE_SYMBOL } from '../constants.js';
+import { DERIVED, STATE_SYMBOL } from '#client/constants';
import { effect_tracking } from '../reactivity/effects.js';
import { active_reaction, captured_signals, set_captured_signals, untrack } from '../runtime.js';
diff --git a/packages/svelte/src/internal/client/dev/validation.js b/packages/svelte/src/internal/client/dev/validation.js
new file mode 100644
index 0000000000..e41e4c4628
--- /dev/null
+++ b/packages/svelte/src/internal/client/dev/validation.js
@@ -0,0 +1,15 @@
+import { invalid_snippet_arguments } from '../../shared/errors.js';
+/**
+ * @param {Node} anchor
+ * @param {...(()=>any)[]} args
+ */
+export function validate_snippet_args(anchor, ...args) {
+ if (typeof anchor !== 'object' || !(anchor instanceof Node)) {
+ invalid_snippet_arguments();
+ }
+ for (let arg of args) {
+ if (typeof arg !== 'function') {
+ invalid_snippet_arguments();
+ }
+ }
+}
diff --git a/packages/svelte/src/internal/client/dom/blocks/await.js b/packages/svelte/src/internal/client/dom/blocks/await.js
index 62b2e4dd0c..99bdc0000c 100644
--- a/packages/svelte/src/internal/client/dom/blocks/await.js
+++ b/packages/svelte/src/internal/client/dom/blocks/await.js
@@ -3,18 +3,23 @@ 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 { flushSync, set_active_effect, set_active_reaction } from '../../runtime.js';
+import {
+ hydrate_next,
+ hydrate_node,
+ hydrating,
+ remove_nodes,
+ set_hydrate_node,
+ set_hydrating
+} from '../hydration.js';
+import { queue_micro_task } from '../task.js';
+import { HYDRATION_START_ELSE, 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;
@@ -107,7 +112,7 @@ export function await_block(node, get_input, pending_fn, then_fn, catch_fn) {
// without this, the DOM does not update until two ticks after the promise
// resolves, which is unexpected behaviour (and somewhat irksome to test)
- flush_sync();
+ flushSync();
}
}
}
@@ -115,6 +120,19 @@ export function await_block(node, get_input, pending_fn, then_fn, catch_fn) {
var effect = block(() => {
if (input === (input = get_input())) return;
+ /** Whether or not there was a hydration mismatch. Needs to be a `let` or else it isn't treeshaken out */
+ // @ts-ignore coercing `anchor` to a `Comment` causes TypeScript and Prettier to fight
+ let mismatch = hydrating && is_promise(input) === (anchor.data === HYDRATION_START_ELSE);
+
+ if (mismatch) {
+ // Hydration mismatch: remove everything inside the anchor and start fresh
+ anchor = remove_nodes();
+
+ set_hydrate_node(anchor);
+ set_hydrating(false);
+ mismatch = true;
+ }
+
if (is_promise(input)) {
var promise = input;
@@ -157,6 +175,11 @@ export function await_block(node, get_input, pending_fn, then_fn, catch_fn) {
update(THEN, false);
}
+ if (mismatch) {
+ // continue in hydration mode
+ set_hydrating(true);
+ }
+
// Set the input to something else, in order to disable the promise callbacks
return () => (input = UNINITIALIZED);
});
diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js
index 7f4f000dce..53060017b9 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 { BOUNDARY_EFFECT, EFFECT_TRANSPARENT } from '#client/constants';
+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/css-props.js b/packages/svelte/src/internal/client/dom/blocks/css-props.js
index 473d35b122..ecbcfd3e83 100644
--- a/packages/svelte/src/internal/client/dom/blocks/css-props.js
+++ b/packages/svelte/src/internal/client/dom/blocks/css-props.js
@@ -1,6 +1,6 @@
/** @import { TemplateNode } from '#client' */
import { render_effect, teardown } from '../../reactivity/effects.js';
-import { hydrate_node, hydrating, set_hydrate_node } from '../hydration.js';
+import { hydrating, set_hydrate_node } from '../hydration.js';
import { get_first_child } from '../operations.js';
/**
diff --git a/packages/svelte/src/internal/client/dom/blocks/each.js b/packages/svelte/src/internal/client/dom/blocks/each.js
index b17090948a..92c953b541 100644
--- a/packages/svelte/src/internal/client/dom/blocks/each.js
+++ b/packages/svelte/src/internal/client/dom/blocks/each.js
@@ -33,9 +33,9 @@ import {
} from '../../reactivity/effects.js';
import { source, mutable_source, internal_set } from '../../reactivity/sources.js';
import { array_from, is_array } from '../../../shared/utils.js';
-import { INERT } from '../../constants.js';
+import { INERT } from '#client/constants';
import { queue_micro_task } from '../task.js';
-import { active_effect, active_reaction, get } from '../../runtime.js';
+import { active_effect, get } from '../../runtime.js';
import { DEV } from 'esm-env';
import { derived_safe_equal } from '../../reactivity/deriveds.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..92c8243478 100644
--- a/packages/svelte/src/internal/client/dom/blocks/html.js
+++ b/packages/svelte/src/internal/client/dom/blocks/html.js
@@ -1,14 +1,15 @@
/** @import { Effect, TemplateNode } from '#client' */
import { FILENAME, HYDRATION_ERROR } from '../../../../constants.js';
-import { block, branch, destroy_effect } from '../../reactivity/effects.js';
+import { remove_effect_dom, template_effect } from '../../reactivity/effects.js';
import { hydrate_next, hydrate_node, hydrating, set_hydrate_node } from '../hydration.js';
import { create_fragment_from_html } from '../reconciler.js';
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';
+import { active_effect } from '../../runtime.js';
/**
* @param {Element} element
@@ -34,89 +35,81 @@ function check_hash(element, server_hash, value) {
/**
* @param {Element | Text | Comment} node
* @param {() => string} get_value
- * @param {boolean} svg
- * @param {boolean} mathml
+ * @param {boolean} [svg]
+ * @param {boolean} [mathml]
* @param {boolean} [skip_warning]
* @returns {void}
*/
-export function html(node, get_value, svg, mathml, skip_warning) {
+export function html(node, get_value, svg = false, mathml = false, skip_warning = false) {
var anchor = node;
var value = '';
- /** @type {Effect | undefined} */
- var effect;
+ template_effect(() => {
+ var effect = /** @type {Effect} */ (active_effect);
- block(() => {
if (value === (value = get_value() ?? '')) {
- if (hydrating) {
- hydrate_next();
- }
+ if (hydrating) hydrate_next();
return;
}
- if (effect !== undefined) {
- destroy_effect(effect);
- effect = undefined;
+ if (effect.nodes_start !== null) {
+ remove_effect_dom(effect.nodes_start, /** @type {TemplateNode} */ (effect.nodes_end));
+ effect.nodes_start = effect.nodes_end = null;
}
if (value === '') return;
- effect = branch(() => {
- if (hydrating) {
- // We're deliberately not trying to repair mismatches between server and client,
- // as it's costly and error-prone (and it's an edge case to have a mismatch anyway)
- var hash = /** @type {Comment} */ (hydrate_node).data;
- var next = hydrate_next();
- var last = next;
-
- while (
- next !== null &&
- (next.nodeType !== 8 || /** @type {Comment} */ (next).data !== '')
- ) {
- last = next;
- next = /** @type {TemplateNode} */ (get_next_sibling(next));
- }
-
- if (next === null) {
- w.hydration_mismatch();
- throw HYDRATION_ERROR;
- }
-
- if (DEV && !skip_warning) {
- check_hash(/** @type {Element} */ (next.parentNode), hash, value);
- }
-
- assign_nodes(hydrate_node, last);
- anchor = set_hydrate_node(next);
- return;
- }
+ if (hydrating) {
+ // We're deliberately not trying to repair mismatches between server and client,
+ // as it's costly and error-prone (and it's an edge case to have a mismatch anyway)
+ var hash = /** @type {Comment} */ (hydrate_node).data;
+ var next = hydrate_next();
+ var last = next;
- var html = value + '';
- if (svg) html = `${html} `;
- else if (mathml) html = `${html} `;
+ while (next !== null && (next.nodeType !== 8 || /** @type {Comment} */ (next).data !== '')) {
+ last = next;
+ next = /** @type {TemplateNode} */ (get_next_sibling(next));
+ }
- // Don't use create_fragment_with_script_from_html here because that would mean script tags are executed.
- // @html is basically `.innerHTML = ...` and that doesn't execute scripts either due to security reasons.
- /** @type {DocumentFragment | Element} */
- var node = create_fragment_from_html(html);
+ if (next === null) {
+ w.hydration_mismatch();
+ throw HYDRATION_ERROR;
+ }
- if (svg || mathml) {
- node = /** @type {Element} */ (get_first_child(node));
+ if (DEV && !skip_warning) {
+ check_hash(/** @type {Element} */ (next.parentNode), hash, value);
}
- assign_nodes(
- /** @type {TemplateNode} */ (get_first_child(node)),
- /** @type {TemplateNode} */ (node.lastChild)
- );
-
- if (svg || mathml) {
- while (get_first_child(node)) {
- anchor.before(/** @type {Node} */ (get_first_child(node)));
- }
- } else {
- anchor.before(node);
+ assign_nodes(hydrate_node, last);
+ anchor = set_hydrate_node(next);
+ return;
+ }
+
+ var html = value + '';
+ if (svg) html = `${html} `;
+ else if (mathml) html = `${html} `;
+
+ // Don't use create_fragment_with_script_from_html here because that would mean script tags are executed.
+ // @html is basically `.innerHTML = ...` and that doesn't execute scripts either due to security reasons.
+ /** @type {DocumentFragment | Element} */
+ var node = create_fragment_from_html(html);
+
+ if (svg || mathml) {
+ node = /** @type {Element} */ (get_first_child(node));
+ }
+
+ assign_nodes(
+ /** @type {TemplateNode} */ (get_first_child(node)),
+ /** @type {TemplateNode} */ (node.lastChild)
+ );
+
+ if (svg || mathml) {
+ while (get_first_child(node)) {
+ anchor.before(/** @type {Node} */ (get_first_child(node)));
}
- });
+ } else {
+ anchor.before(node);
+ }
});
}
diff --git a/packages/svelte/src/internal/client/dom/blocks/if.js b/packages/svelte/src/internal/client/dom/blocks/if.js
index 36790c05c1..925abb9d9d 100644
--- a/packages/svelte/src/internal/client/dom/blocks/if.js
+++ b/packages/svelte/src/internal/client/dom/blocks/if.js
@@ -1,5 +1,5 @@
/** @import { Effect, TemplateNode } from '#client' */
-import { EFFECT_TRANSPARENT } from '../../constants.js';
+import { EFFECT_TRANSPARENT } from '#client/constants';
import {
hydrate_next,
hydrate_node,
@@ -9,16 +9,16 @@ import {
set_hydrating
} from '../hydration.js';
import { block, branch, pause_effect, resume_effect } from '../../reactivity/effects.js';
-import { HYDRATION_START_ELSE, UNINITIALIZED } from '../../../../constants.js';
+import { HYDRATION_START, HYDRATION_START_ELSE, UNINITIALIZED } from '../../../../constants.js';
/**
* @param {TemplateNode} node
- * @param {(branch: (fn: (anchor: Node) => void, flag?: boolean) => void) => void} fn
- * @param {boolean} [elseif] True if this is an `{:else if ...}` block rather than an `{#if ...}`, as that affects which transitions are considered 'local'
+ * @param {(branch: (fn: (anchor: Node, elseif?: [number,number]) => void, flag?: boolean) => void) => void} fn
+ * @param {[number,number]} [elseif]
* @returns {void}
*/
-export function if_block(node, fn, elseif = false) {
- if (hydrating) {
+export function if_block(node, fn, [root_index, hydrate_index] = [0, 0]) {
+ if (hydrating && root_index === 0) {
hydrate_next();
}
@@ -33,26 +33,44 @@ export function if_block(node, fn, elseif = false) {
/** @type {UNINITIALIZED | boolean | null} */
var condition = UNINITIALIZED;
- var flags = elseif ? EFFECT_TRANSPARENT : 0;
+ var flags = root_index > 0 ? EFFECT_TRANSPARENT : 0;
var has_branch = false;
- const set_branch = (/** @type {(anchor: Node) => void} */ fn, flag = true) => {
+ const set_branch = (
+ /** @type {(anchor: Node, elseif?: [number,number]) => void} */ fn,
+ flag = true
+ ) => {
has_branch = true;
update_branch(flag, fn);
};
const update_branch = (
/** @type {boolean | null} */ new_condition,
- /** @type {null | ((anchor: Node) => void)} */ fn
+ /** @type {null | ((anchor: Node, elseif?: [number,number]) => void)} */ fn
) => {
if (condition === (condition = new_condition)) return;
/** Whether or not there was a hydration mismatch. Needs to be a `let` or else it isn't treeshaken out */
let mismatch = false;
- if (hydrating) {
- const is_else = /** @type {Comment} */ (anchor).data === HYDRATION_START_ELSE;
+ if (hydrating && hydrate_index !== -1) {
+ if (root_index === 0) {
+ const data = /** @type {Comment} */ (anchor).data;
+ if (data === HYDRATION_START) {
+ hydrate_index = 0;
+ } else if (data === HYDRATION_START_ELSE) {
+ hydrate_index = Infinity;
+ } else {
+ hydrate_index = parseInt(data.substring(1));
+ if (hydrate_index !== hydrate_index) {
+ // if hydrate_index is NaN
+ // we set an invalid index to force mismatch
+ hydrate_index = condition ? Infinity : -1;
+ }
+ }
+ }
+ const is_else = hydrate_index > root_index;
if (!!condition === is_else) {
// Hydration mismatch: remove everything inside the anchor and start fresh.
@@ -62,6 +80,7 @@ export function if_block(node, fn, elseif = false) {
set_hydrate_node(anchor);
set_hydrating(false);
mismatch = true;
+ hydrate_index = -1; // ignore hydration in next else if
}
}
@@ -81,7 +100,7 @@ export function if_block(node, fn, elseif = false) {
if (alternate_effect) {
resume_effect(alternate_effect);
} else if (fn) {
- alternate_effect = branch(() => fn(anchor));
+ alternate_effect = branch(() => fn(anchor, [root_index + 1, hydrate_index]));
}
if (consequent_effect) {
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..c6dce26bfe 100644
--- a/packages/svelte/src/internal/client/dom/blocks/snippet.js
+++ b/packages/svelte/src/internal/client/dom/blocks/snippet.js
@@ -1,12 +1,12 @@
/** @import { Snippet } from 'svelte' */
/** @import { Effect, TemplateNode } from '#client' */
/** @import { Getters } from '#shared' */
-import { EFFECT_TRANSPARENT } from '../../constants.js';
+import { EFFECT_TRANSPARENT } from '#client/constants';
import { branch, block, destroy_effect, teardown } from '../../reactivity/effects.js';
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';
@@ -15,6 +15,7 @@ import * as e from '../../errors.js';
import { DEV } from 'esm-env';
import { get_first_child, get_next_sibling } from '../operations.js';
import { noop } from '../../../shared/utils.js';
+import { prevent_snippet_stringification } from '../../../shared/validate.js';
/**
* @template {(node: TemplateNode, ...args: any[]) => void} SnippetFn
@@ -60,7 +61,7 @@ export function snippet(node, get_snippet, ...args) {
* @param {(node: TemplateNode, ...args: any[]) => void} fn
*/
export function wrap_snippet(component, fn) {
- return (/** @type {TemplateNode} */ node, /** @type {any[]} */ ...args) => {
+ const snippet = (/** @type {TemplateNode} */ node, /** @type {any[]} */ ...args) => {
var previous_component_function = dev_current_component_function;
set_dev_current_component_function(component);
@@ -70,6 +71,10 @@ export function wrap_snippet(component, fn) {
set_dev_current_component_function(previous_component_function);
}
};
+
+ prevent_snippet_stringification(snippet);
+
+ return snippet;
}
/**
diff --git a/packages/svelte/src/internal/client/dom/blocks/svelte-component.js b/packages/svelte/src/internal/client/dom/blocks/svelte-component.js
index 72157eaa40..ad21436505 100644
--- a/packages/svelte/src/internal/client/dom/blocks/svelte-component.js
+++ b/packages/svelte/src/internal/client/dom/blocks/svelte-component.js
@@ -1,5 +1,5 @@
/** @import { TemplateNode, Dom, Effect } from '#client' */
-import { EFFECT_TRANSPARENT } from '../../constants.js';
+import { EFFECT_TRANSPARENT } from '#client/constants';
import { block, branch, pause_effect } from '../../reactivity/effects.js';
import { hydrate_next, hydrate_node, hydrating } from '../hydration.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..43f669e844 100644
--- a/packages/svelte/src/internal/client/dom/blocks/svelte-element.js
+++ b/packages/svelte/src/internal/client/dom/blocks/svelte-element.js
@@ -17,9 +17,10 @@ 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 { EFFECT_TRANSPARENT } from '#client/constants';
import { assign_nodes } from '../template.js';
import { is_raw_text_element } from '../../../../utils.js';
diff --git a/packages/svelte/src/internal/client/dom/blocks/svelte-head.js b/packages/svelte/src/internal/client/dom/blocks/svelte-head.js
index e3e3eacad7..db2a0c4ef1 100644
--- a/packages/svelte/src/internal/client/dom/blocks/svelte-head.js
+++ b/packages/svelte/src/internal/client/dom/blocks/svelte-head.js
@@ -2,7 +2,7 @@
import { hydrate_node, hydrating, set_hydrate_node, set_hydrating } from '../hydration.js';
import { create_text, get_first_child, get_next_sibling } from '../operations.js';
import { block } from '../../reactivity/effects.js';
-import { HEAD_EFFECT } from '../../constants.js';
+import { HEAD_EFFECT } from '#client/constants';
import { HYDRATION_START } from '../../../../constants.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..f63f55cc6e 100644
--- a/packages/svelte/src/internal/client/dom/elements/attributes.js
+++ b/packages/svelte/src/internal/client/dom/elements/attributes.js
@@ -1,10 +1,10 @@
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';
import * as w from '../../warnings.js';
-import { LOADING_ATTR_SYMBOL } from '../../constants.js';
+import { LOADING_ATTR_SYMBOL } from '#client/constants';
import { queue_idle_task } from '../task.js';
import { is_capture_event, is_delegated, normalize_attribute } from '../../../../utils.js';
import {
@@ -14,6 +14,15 @@ import {
set_active_reaction
} from '../../runtime.js';
import { clsx } from '../../../shared/attributes.js';
+import { set_class } from './class.js';
+import { set_style } from './style.js';
+import { NAMESPACE_HTML } from '../../../../constants.js';
+
+export const CLASS = Symbol('class');
+export const STYLE = Symbol('style');
+
+const IS_CUSTOM_ELEMENT = Symbol('is custom element');
+const IS_HTML = Symbol('is html');
/**
* The value/checked attribute in the template actually corresponds to the defaultValue property, so we need
@@ -59,8 +68,7 @@ export function remove_input_defaults(input) {
* @param {any} value
*/
export function set_value(element, value) {
- // @ts-expect-error
- var attributes = (element.__attributes ??= {});
+ var attributes = get_attributes(element);
if (
attributes.value ===
@@ -68,14 +76,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 ?? '';
}
/**
@@ -83,8 +91,7 @@ export function set_value(element, value) {
* @param {boolean} checked
*/
export function set_checked(element, checked) {
- // @ts-expect-error
- var attributes = (element.__attributes ??= {});
+ var attributes = get_attributes(element);
if (
attributes.checked ===
@@ -147,8 +154,7 @@ export function set_default_value(element, value) {
* @param {boolean} [skip_warning]
*/
export function set_attribute(element, attribute, value, skip_warning) {
- // @ts-expect-error
- var attributes = (element.__attributes ??= {});
+ var attributes = get_attributes(element);
if (hydrating) {
attributes[attribute] = element.getAttribute(attribute);
@@ -172,11 +178,6 @@ export function set_attribute(element, attribute, value, skip_warning) {
if (attributes[attribute] === (attributes[attribute] = value)) return;
- if (attribute === 'style' && '__styles' in element) {
- // reset styles to force style: directive to update
- element.__styles = {};
- }
-
if (attribute === 'loading') {
// @ts-expect-error
element[LOADING_ATTR_SYMBOL] = value;
@@ -214,19 +215,29 @@ export function set_custom_element_data(node, prop, value) {
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);
+
try {
if (
+ // `style` should use `set_attribute` rather than the setter
+ prop !== 'style' &&
// Don't compute setters for custom elements while they aren't registered yet,
// because during their upgrade/instantiation they might add more setters.
// Instead, fall back to a simple "an object, then set as property" heuristic.
- setters_cache.has(node.nodeName) ||
+ (setters_cache.has(node.nodeName) ||
// customElements may not be available in browser extension contexts
!customElements ||
customElements.get(node.tagName.toLowerCase())
? get_setters(node).includes(prop)
- : value && typeof value === 'object'
+ : value && typeof value === 'object')
) {
// @ts-expect-error
node[prop] = value;
@@ -239,29 +250,34 @@ 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);
+ }
}
}
/**
* Spreads attributes onto a DOM element, taking into account the currently set attributes
* @param {Element & ElementCSSInlineStyle} element
- * @param {Record | undefined} prev
- * @param {Record} next New attributes - this function mutates this object
+ * @param {Record | undefined} prev
+ * @param {Record} next New attributes - this function mutates this object
* @param {string} [css_hash]
- * @param {boolean} [preserve_attribute_case]
- * @param {boolean} [is_custom_element]
* @param {boolean} [skip_warning]
* @returns {Record}
*/
-export function set_attributes(
- element,
- prev,
- next,
- css_hash,
- preserve_attribute_case = false,
- is_custom_element = false,
- skip_warning = false
-) {
+export function set_attributes(element, prev, next, css_hash, skip_warning = false) {
+ var attributes = get_attributes(element);
+
+ var is_custom_element = attributes[IS_CUSTOM_ELEMENT];
+ var preserve_attribute_case = !attributes[IS_HTML];
+
+ // 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';
@@ -273,17 +289,16 @@ export function set_attributes(
if (next.class) {
next.class = clsx(next.class);
+ } else if (css_hash || next[CLASS]) {
+ next.class = null; /* force call to set_class() */
}
- if (css_hash !== undefined) {
- next.class = next.class ? next.class + ' ' + css_hash : css_hash;
+ if (next[STYLE]) {
+ next.style ??= null; /* force call to set_style() */
}
var setters = get_setters(element);
- // @ts-expect-error
- var attributes = /** @type {Record} **/ (element.__attributes ??= {});
-
// since key is captured we use const
for (const key in next) {
// let instead of var because referenced in a closure
@@ -308,6 +323,21 @@ export function set_attributes(
continue;
}
+ if (key === 'class') {
+ var is_html = element.namespaceURI === 'http://www.w3.org/1999/xhtml';
+ set_class(element, is_html, value, css_hash, prev?.[CLASS], next[CLASS]);
+ current[key] = value;
+ current[CLASS] = next[CLASS];
+ continue;
+ }
+
+ if (key === 'style') {
+ set_style(element, value, prev?.[STYLE], next[STYLE]);
+ current[key] = value;
+ current[STYLE] = next[STYLE];
+ continue;
+ }
+
var prev_value = current[key];
if (value === prev_value) continue;
@@ -359,13 +389,15 @@ export function set_attributes(
// @ts-ignore
element[`__${event_name}`] = undefined;
}
- } else if (key === 'style' && value != null) {
- element.style.cssText = value + '';
+ } else if (key === 'style') {
+ // avoid using the setter
+ set_attribute(element, key, 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 +414,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,22 +437,32 @@ 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, skip_warning);
}
}
- if (key === 'style' && '__styles' in element) {
- // reset styles to force style: directive to update
- element.__styles = {};
- }
+ }
+
+ if (is_hydrating_custom_element) {
+ set_hydrating(true);
}
return current;
}
+/**
+ *
+ * @param {Element} element
+ */
+function get_attributes(element) {
+ return /** @type {Record} **/ (
+ // @ts-expect-error
+ element.__attributes ??= {
+ [IS_CUSTOM_ELEMENT]: element.nodeName.includes('-'),
+ [IS_HTML]: element.namespaceURI === NAMESPACE_HTML
+ }
+ );
+}
+
/** @type {Map} */
var setters_cache = new Map();
@@ -503,28 +548,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 ec123d3968..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
@@ -259,6 +260,15 @@ export function bind_files(input, get, set = get) {
set(input.files);
});
+ if (
+ // If we are hydrating and the value has since changed,
+ // then use the updated value from the input instead.
+ hydrating &&
+ input.files
+ ) {
+ set(input.files);
+ }
+
render_effect(() => {
input.files = get();
});
diff --git a/packages/svelte/src/internal/client/dom/elements/bindings/media.js b/packages/svelte/src/internal/client/dom/elements/bindings/media.js
index 444a1b3439..30a8dac1af 100644
--- a/packages/svelte/src/internal/client/dom/elements/bindings/media.js
+++ b/packages/svelte/src/internal/client/dom/elements/bindings/media.js
@@ -1,4 +1,3 @@
-import { hydrating } from '../../hydration.js';
import { render_effect, effect, teardown } from '../../../reactivity/effects.js';
import { listen } from './shared.js';
@@ -52,7 +51,10 @@ export function bind_current_time(media, get, set = get) {
}
});
- teardown(() => cancelAnimationFrame(raf_id));
+ teardown(() => {
+ cancelAnimationFrame(raf_id);
+ media.removeEventListener('timeupdate', callback);
+ });
}
/**
diff --git a/packages/svelte/src/internal/client/dom/elements/bindings/this.js b/packages/svelte/src/internal/client/dom/elements/bindings/this.js
index 56b0a56e71..e9bbcedc6f 100644
--- a/packages/svelte/src/internal/client/dom/elements/bindings/this.js
+++ b/packages/svelte/src/internal/client/dom/elements/bindings/this.js
@@ -1,4 +1,4 @@
-import { STATE_SYMBOL } from '../../../constants.js';
+import { STATE_SYMBOL } from '#client/constants';
import { effect, render_effect } from '../../../reactivity/effects.js';
import { untrack } from '../../../runtime.js';
import { queue_micro_task } from '../../task.js';
diff --git a/packages/svelte/src/internal/client/dom/elements/class.js b/packages/svelte/src/internal/client/dom/elements/class.js
index 62ffb6d14b..fc081b8956 100644
--- a/packages/svelte/src/internal/client/dom/elements/class.js
+++ b/packages/svelte/src/internal/client/dom/elements/class.js
@@ -1,120 +1,51 @@
+import { to_class } from '../../../shared/attributes.js';
import { hydrating } from '../hydration.js';
/**
- * @param {SVGElement} dom
- * @param {string} value
+ * @param {Element} dom
+ * @param {boolean | number} is_html
+ * @param {string | null} value
* @param {string} [hash]
- * @returns {void}
+ * @param {Record} [prev_classes]
+ * @param {Record} [next_classes]
+ * @returns {Record | undefined}
*/
-export function set_svg_class(dom, value, hash) {
+export function set_class(dom, is_html, value, hash, prev_classes, next_classes) {
// @ts-expect-error need to add __className to patched prototype
- var prev_class_name = dom.__className;
- var next_class_name = to_class(value, hash);
+ var prev = dom.__className;
- if (hydrating && dom.getAttribute('class') === next_class_name) {
- // In case of hydration don't reset the class as it's already correct.
- // @ts-expect-error need to add __className to patched prototype
- dom.__className = next_class_name;
- } else if (
- prev_class_name !== next_class_name ||
- (hydrating && dom.getAttribute('class') !== next_class_name)
+ if (
+ hydrating ||
+ prev !== value ||
+ prev === undefined // for edge case of `class={undefined}`
) {
- if (next_class_name === '') {
- dom.removeAttribute('class');
- } else {
- dom.setAttribute('class', next_class_name);
+ var next_class_name = to_class(value, hash, next_classes);
+
+ if (!hydrating || next_class_name !== dom.getAttribute('class')) {
+ // Removing the attribute when the value is only an empty string causes
+ // performance issues vs simply making the className an empty string. So
+ // we should only remove the class if the the value is nullish
+ // and there no hash/directives :
+ if (next_class_name == null) {
+ dom.removeAttribute('class');
+ } else if (is_html) {
+ dom.className = next_class_name;
+ } else {
+ dom.setAttribute('class', next_class_name);
+ }
}
// @ts-expect-error need to add __className to patched prototype
- dom.__className = next_class_name;
- }
-}
-
-/**
- * @param {MathMLElement} dom
- * @param {string} value
- * @param {string} [hash]
- * @returns {void}
- */
-export function set_mathml_class(dom, value, hash) {
- // @ts-expect-error need to add __className to patched prototype
- var prev_class_name = dom.__className;
- var next_class_name = to_class(value, hash);
-
- if (hydrating && dom.getAttribute('class') === next_class_name) {
- // In case of hydration don't reset the class as it's already correct.
- // @ts-expect-error need to add __className to patched prototype
- dom.__className = next_class_name;
- } else if (
- prev_class_name !== next_class_name ||
- (hydrating && dom.getAttribute('class') !== next_class_name)
- ) {
- if (next_class_name === '') {
- dom.removeAttribute('class');
- } else {
- dom.setAttribute('class', next_class_name);
+ dom.__className = value;
+ } else if (next_classes && prev_classes !== next_classes) {
+ for (var key in next_classes) {
+ var is_present = !!next_classes[key];
+
+ if (prev_classes == null || is_present !== !!prev_classes[key]) {
+ dom.classList.toggle(key, is_present);
+ }
}
-
- // @ts-expect-error need to add __className to patched prototype
- dom.__className = next_class_name;
}
-}
-/**
- * @param {HTMLElement} dom
- * @param {string} value
- * @param {string} [hash]
- * @returns {void}
- */
-export function set_class(dom, value, hash) {
- // @ts-expect-error need to add __className to patched prototype
- var prev_class_name = dom.__className;
- var next_class_name = to_class(value, hash);
-
- if (hydrating && dom.className === next_class_name) {
- // In case of hydration don't reset the class as it's already correct.
- // @ts-expect-error need to add __className to patched prototype
- dom.__className = next_class_name;
- } else if (
- prev_class_name !== next_class_name ||
- (hydrating && dom.className !== next_class_name)
- ) {
- // Removing the attribute when the value is only an empty string causes
- // peformance issues vs simply making the className an empty string. So
- // we should only remove the class if the the value is nullish.
- if (value == null && !hash) {
- dom.removeAttribute('class');
- } else {
- dom.className = next_class_name;
- }
-
- // @ts-expect-error need to add __className to patched prototype
- dom.__className = next_class_name;
- }
-}
-
-/**
- * @template V
- * @param {V} value
- * @param {string} [hash]
- * @returns {string | V}
- */
-function to_class(value, hash) {
- return (value == null ? '' : value) + (hash ? ' ' + hash : '');
-}
-
-/**
- * @param {Element} dom
- * @param {string} class_name
- * @param {boolean} value
- * @returns {void}
- */
-export function toggle_class(dom, class_name, value) {
- if (value) {
- if (dom.classList.contains(class_name)) return;
- dom.classList.add(class_name);
- } else {
- if (!dom.classList.contains(class_name)) return;
- dom.classList.remove(class_name);
- }
+ return next_classes;
}
diff --git a/packages/svelte/src/internal/client/dom/elements/custom-element.js b/packages/svelte/src/internal/client/dom/elements/custom-element.js
index 6195b2c561..2d118bfab3 100644
--- a/packages/svelte/src/internal/client/dom/elements/custom-element.js
+++ b/packages/svelte/src/internal/client/dom/elements/custom-element.js
@@ -1,5 +1,5 @@
import { createClassComponent } from '../../../../legacy/legacy-client.js';
-import { destroy_effect, effect_root, render_effect } from '../../reactivity/effects.js';
+import { effect_root, render_effect } from '../../reactivity/effects.js';
import { append } from '../template.js';
import { define_property, get_descriptor, object_keys } from '../../../shared/utils.js';
diff --git a/packages/svelte/src/internal/client/dom/elements/events.js b/packages/svelte/src/internal/client/dom/elements/events.js
index f2038f96ad..3374fe713f 100644
--- a/packages/svelte/src/internal/client/dom/elements/events.js
+++ b/packages/svelte/src/internal/client/dom/elements/events.js
@@ -1,4 +1,3 @@
-/** @import { Location } from 'locate-character' */
import { teardown } from '../../reactivity/effects.js';
import { define_property, is_array } from '../../../shared/utils.js';
import { hydrating } from '../hydration.js';
@@ -49,10 +48,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 +62,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 +107,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}
*/
@@ -237,7 +236,13 @@ export function handle_event_propagation(event) {
// @ts-expect-error
var delegated = current_target['__' + event_name];
- if (delegated !== undefined && !(/** @type {any} */ (current_target).disabled)) {
+ if (
+ delegated != null &&
+ (!(/** @type {any} */ (current_target).disabled) ||
+ // DOM could've been updated already by the time this is reached, so we check this as well
+ // -> the target could not have been disabled because it emits the event in the first place
+ event.target === current_target)
+ ) {
if (is_array(delegated)) {
var [fn, ...data] = delegated;
fn.apply(current_target, [event, ...data]);
@@ -305,13 +310,11 @@ export function apply(
error = e;
}
- if (typeof handler === 'function') {
- handler.apply(element, args);
- } else if (has_side_effects || handler != null || error) {
+ if (typeof handler !== 'function' && (has_side_effects || handler != null || error)) {
const filename = component?.[FILENAME];
const location = loc ? ` at ${filename}:${loc[0]}:${loc[1]}` : ` in ${filename}`;
-
- const event_name = args[0].type;
+ const phase = args[0]?.eventPhase < Event.BUBBLING_PHASE ? 'capture' : '';
+ const event_name = args[0]?.type + phase;
const description = `\`${event_name}\` handler${location}`;
const suggestion = remove_parens ? 'remove the trailing `()`' : 'add a leading `() =>`';
@@ -321,4 +324,5 @@ export function apply(
throw error;
}
}
+ handler?.apply(element, args);
}
diff --git a/packages/svelte/src/internal/client/dom/elements/style.js b/packages/svelte/src/internal/client/dom/elements/style.js
index 34531029c9..3e05eec30e 100644
--- a/packages/svelte/src/internal/client/dom/elements/style.js
+++ b/packages/svelte/src/internal/client/dom/elements/style.js
@@ -1,22 +1,57 @@
+import { to_style } from '../../../shared/attributes.js';
+import { hydrating } from '../hydration.js';
+
/**
- * @param {HTMLElement} dom
- * @param {string} key
- * @param {string} value
- * @param {boolean} [important]
+ * @param {Element & ElementCSSInlineStyle} dom
+ * @param {Record} prev
+ * @param {Record} next
+ * @param {string} [priority]
*/
-export function set_style(dom, key, value, important) {
- // @ts-expect-error
- var styles = (dom.__styles ??= {});
+function update_styles(dom, prev = {}, next, priority) {
+ for (var key in next) {
+ var value = next[key];
- if (styles[key] === value) {
- return;
+ if (prev[key] !== value) {
+ if (next[key] == null) {
+ dom.style.removeProperty(key);
+ } else {
+ dom.style.setProperty(key, value, priority);
+ }
+ }
}
+}
- styles[key] = value;
+/**
+ * @param {Element & ElementCSSInlineStyle} dom
+ * @param {string | null} value
+ * @param {Record | [Record, Record]} [prev_styles]
+ * @param {Record | [Record, Record]} [next_styles]
+ */
+export function set_style(dom, value, prev_styles, next_styles) {
+ // @ts-expect-error
+ var prev = dom.__style;
+
+ if (hydrating || prev !== value) {
+ var next_style_attr = to_style(value, next_styles);
- if (value == null) {
- dom.style.removeProperty(key);
- } else {
- dom.style.setProperty(key, value, important ? 'important' : '');
+ if (!hydrating || next_style_attr !== dom.getAttribute('style')) {
+ if (next_style_attr == null) {
+ dom.removeAttribute('style');
+ } else {
+ dom.style.cssText = next_style_attr;
+ }
+ }
+
+ // @ts-expect-error
+ dom.__style = value;
+ } else if (next_styles) {
+ if (Array.isArray(next_styles)) {
+ update_styles(dom, prev_styles?.[0], next_styles[0]);
+ update_styles(dom, prev_styles?.[1], next_styles[1], 'important');
+ } else {
+ update_styles(dom, prev_styles, next_styles);
+ }
}
+
+ return next_styles;
}
diff --git a/packages/svelte/src/internal/client/dom/elements/transitions.js b/packages/svelte/src/internal/client/dom/elements/transitions.js
index b3c16cdd08..cc895cbccb 100644
--- a/packages/svelte/src/internal/client/dom/elements/transitions.js
+++ b/packages/svelte/src/internal/client/dom/elements/transitions.js
@@ -12,8 +12,9 @@ import { loop } from '../../loop.js';
import { should_intro } from '../../render.js';
import { current_each_item } from '../blocks/each.js';
import { TRANSITION_GLOBAL, TRANSITION_IN, TRANSITION_OUT } from '../../../../constants.js';
-import { BLOCK_EFFECT, EFFECT_RAN, EFFECT_TRANSPARENT } from '../../constants.js';
+import { BLOCK_EFFECT, EFFECT_RAN, EFFECT_TRANSPARENT } from '#client/constants';
import { queue_micro_task } from '../task.js';
+import { without_reactive_context } from './bindings/shared.js';
/**
* @param {Element} element
@@ -21,7 +22,9 @@ import { queue_micro_task } from '../task.js';
* @returns {void}
*/
function dispatch_event(element, type) {
- element.dispatchEvent(new CustomEvent(type));
+ without_reactive_context(() => {
+ element.dispatchEvent(new CustomEvent(type));
+ });
}
/**
diff --git a/packages/svelte/src/internal/client/dom/legacy/event-modifiers.js b/packages/svelte/src/internal/client/dom/legacy/event-modifiers.js
index 918832dfa5..2e5312f1b0 100644
--- a/packages/svelte/src/internal/client/dom/legacy/event-modifiers.js
+++ b/packages/svelte/src/internal/client/dom/legacy/event-modifiers.js
@@ -1,4 +1,3 @@
-/** @import { ActionReturn } from 'svelte/action' */
import { noop } from '../../../shared/utils.js';
import { user_pre_effect } from '../../reactivity/effects.js';
import { on } from '../elements/events.js';
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..aae44d4b39 100644
--- a/packages/svelte/src/internal/client/dom/operations.js
+++ b/packages/svelte/src/internal/client/dom/operations.js
@@ -2,7 +2,7 @@
import { hydrate_node, hydrating, set_hydrate_node } from './hydration.js';
import { DEV } from 'esm-env';
import { init_array_prototype_warnings } from '../dev/equality.js';
-import { get_descriptor } from '../../shared/utils.js';
+import { get_descriptor, is_extensible } from '../../shared/utils.js';
// export these for reference in the compiled code, making global name deduplication unnecessary
/** @type {Window} */
@@ -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,29 +30,35 @@ export function init_operations() {
$window = window;
$document = document;
+ is_firefox = /Firefox/.test(navigator.userAgent);
var element_prototype = Element.prototype;
var node_prototype = Node.prototype;
+ var text_prototype = Text.prototype;
// @ts-ignore
first_child_getter = get_descriptor(node_prototype, 'firstChild').get;
// @ts-ignore
next_sibling_getter = get_descriptor(node_prototype, 'nextSibling').get;
- // the following assignments improve perf of lookups on DOM nodes
- // @ts-expect-error
- element_prototype.__click = undefined;
- // @ts-expect-error
- element_prototype.__className = '';
- // @ts-expect-error
- element_prototype.__attributes = null;
- // @ts-expect-error
- element_prototype.__styles = null;
- // @ts-expect-error
- element_prototype.__e = undefined;
-
- // @ts-expect-error
- Text.prototype.__t = undefined;
+ if (is_extensible(element_prototype)) {
+ // the following assignments improve perf of lookups on DOM nodes
+ // @ts-expect-error
+ element_prototype.__click = undefined;
+ // @ts-expect-error
+ element_prototype.__className = undefined;
+ // @ts-expect-error
+ element_prototype.__attributes = null;
+ // @ts-expect-error
+ element_prototype.__style = undefined;
+ // @ts-expect-error
+ element_prototype.__e = undefined;
+ }
+
+ if (is_extensible(text_prototype)) {
+ // @ts-expect-error
+ text_prototype.__t = undefined;
+ }
if (DEV) {
// @ts-expect-error
diff --git a/packages/svelte/src/internal/client/dom/task.js b/packages/svelte/src/internal/client/dom/task.js
index acb5a5b117..48a2fbe660 100644
--- a/packages/svelte/src/internal/client/dom/task.js
+++ b/packages/svelte/src/internal/client/dom/task.js
@@ -1,30 +1,26 @@
import { run_all } from '../../shared/utils.js';
// Fallback for when requestIdleCallback is not available
-export const request_idle_callback =
+const request_idle_callback =
typeof requestIdleCallback === 'undefined'
? (/** @type {() => void} */ cb) => setTimeout(cb, 1)
: requestIdleCallback;
-let is_micro_task_queued = false;
-let is_idle_task_queued = false;
-
/** @type {Array<() => void>} */
-let current_queued_micro_tasks = [];
+let micro_tasks = [];
+
/** @type {Array<() => void>} */
-let current_queued_idle_tasks = [];
+let idle_tasks = [];
-function process_micro_tasks() {
- is_micro_task_queued = false;
- const tasks = current_queued_micro_tasks.slice();
- current_queued_micro_tasks = [];
+function run_micro_tasks() {
+ var tasks = micro_tasks;
+ micro_tasks = [];
run_all(tasks);
}
-function process_idle_tasks() {
- is_idle_task_queued = false;
- const tasks = current_queued_idle_tasks.slice();
- current_queued_idle_tasks = [];
+function run_idle_tasks() {
+ var tasks = idle_tasks;
+ idle_tasks = [];
run_all(tasks);
}
@@ -32,32 +28,33 @@ function process_idle_tasks() {
* @param {() => void} fn
*/
export function queue_micro_task(fn) {
- if (!is_micro_task_queued) {
- is_micro_task_queued = true;
- queueMicrotask(process_micro_tasks);
+ if (micro_tasks.length === 0) {
+ queueMicrotask(run_micro_tasks);
}
- current_queued_micro_tasks.push(fn);
+
+ micro_tasks.push(fn);
}
/**
* @param {() => void} fn
*/
export function queue_idle_task(fn) {
- if (!is_idle_task_queued) {
- is_idle_task_queued = true;
- request_idle_callback(process_idle_tasks);
+ if (idle_tasks.length === 0) {
+ request_idle_callback(run_idle_tasks);
}
- current_queued_idle_tasks.push(fn);
+
+ idle_tasks.push(fn);
}
/**
* Synchronously run any queued tasks.
*/
export function flush_tasks() {
- if (is_micro_task_queued) {
- process_micro_tasks();
+ if (micro_tasks.length > 0) {
+ run_micro_tasks();
}
- if (is_idle_task_queued) {
- process_idle_tasks();
+
+ if (idle_tasks.length > 0) {
+ run_idle_tasks();
}
}
diff --git a/packages/svelte/src/internal/client/dom/template.js b/packages/svelte/src/internal/client/dom/template.js
index bcbae393ec..de2df62c92 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,25 @@ export function append(anchor, dom) {
anchor.before(/** @type {Node} */ (dom));
}
+
+/**
+ * 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(`#`)
+ ) {
+ const id = hydrate_node.textContent.substring(1);
+ hydrate_next();
+ return id;
+ }
+
+ // @ts-expect-error This way we ensure the id is unique even across Svelte runtimes
+ (window.__svelte ??= {}).uid ??= 1;
+
+ // @ts-expect-error
+ return `c${window.__svelte.uid++}`;
+}
diff --git a/packages/svelte/src/internal/client/errors.js b/packages/svelte/src/internal/client/errors.js
index 682816e1d6..429dd99da9 100644
--- a/packages/svelte/src/internal/client/errors.js
+++ b/packages/svelte/src/internal/client/errors.js
@@ -54,15 +54,14 @@ export function bind_not_bindable(key, component, name) {
}
/**
- * %parent% called `%method%` on an instance of %component%, which is no longer valid in Svelte 5
- * @param {string} parent
+ * Calling `%method%` on a component instance (of %component%) is no longer valid in Svelte 5
* @param {string} method
* @param {string} component
* @returns {never}
*/
-export function component_api_changed(parent, method, component) {
+export function component_api_changed(method, component) {
if (DEV) {
- const error = new Error(`component_api_changed\n${parent} called \`${method}\` on an instance of ${component}, which is no longer valid in Svelte 5\nhttps://svelte.dev/e/component_api_changed`);
+ const error = new Error(`component_api_changed\nCalling \`${method}\` on a component instance (of ${component}) is no longer valid in Svelte 5\nhttps://svelte.dev/e/component_api_changed`);
error.name = 'Svelte error';
throw error;
@@ -307,21 +306,6 @@ export function state_prototype_fixed() {
}
}
-/**
- * Reading state that was created inside the same derived is forbidden. Consider using `untrack` to read locally created state
- * @returns {never}
- */
-export function state_unsafe_local_read() {
- if (DEV) {
- const error = new Error(`state_unsafe_local_read\nReading state that was created inside the same derived is forbidden. Consider using \`untrack\` to read locally created state\nhttps://svelte.dev/e/state_unsafe_local_read`);
-
- error.name = 'Svelte error';
- throw error;
- } else {
- throw new Error(`https://svelte.dev/e/state_unsafe_local_read`);
- }
-}
-
/**
* Updating state inside a derived or a template expression is forbidden. If the value should not be reactive, declare it without `$state`
* @returns {never}
diff --git a/packages/svelte/src/internal/client/index.js b/packages/svelte/src/internal/client/index.js
index 2bf58c51f7..14d6e29f5b 100644
--- a/packages/svelte/src/internal/client/index.js
+++ b/packages/svelte/src/internal/client/index.js
@@ -1,19 +1,14 @@
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';
export { hmr } from './dev/hmr.js';
-export {
- ADD_OWNER,
- add_owner,
- mark_module_start,
- mark_module_end,
- add_owner_effect,
- skip_ownership_validation
-} from './dev/ownership.js';
+export { create_ownership_validator } from './dev/ownership.js';
export { check_target, legacy_api } from './dev/legacy.js';
export { trace } from './dev/tracing.js';
export { inspect } from './dev/inspect.js';
+export { validate_snippet_args } from './dev/validation.js';
export { await_block as await } from './dom/blocks/await.js';
export { if_block as if } from './dom/blocks/if.js';
export { key_block as key } from './dom/blocks/key.js';
@@ -33,14 +28,15 @@ export {
set_attributes,
set_custom_element_data,
set_xlink_attribute,
- handle_lazy_img,
set_value,
set_checked,
set_selected,
set_default_checked,
- set_default_value
+ set_default_value,
+ CLASS,
+ STYLE
} from './dom/elements/attributes.js';
-export { set_class, set_svg_class, set_mathml_class, toggle_class } from './dom/elements/class.js';
+export { set_class } from './dom/elements/class.js';
export { apply, event, delegate, replay_events } from './dom/elements/events.js';
export { autofocus, remove_textarea_child } from './dom/elements/misc.js';
export { set_style } from './dom/elements/style.js';
@@ -95,9 +91,10 @@ 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 { user_derived as derived, derived_safe_equal } from './reactivity/deriveds.js';
export {
effect_tracking,
effect_root,
@@ -109,7 +106,7 @@ export {
user_effect,
user_pre_effect
} from './reactivity/effects.js';
-export { mutable_state, mutate, set, state } from './reactivity/sources.js';
+export { mutable_source, mutate, set, state, update, update_pre } from './reactivity/sources.js';
export {
prop,
rest_props,
@@ -135,20 +132,12 @@ export {
get,
safe_get,
invalidate_inner_signals,
- flush_sync,
+ flushSync as flush,
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';
@@ -168,7 +157,8 @@ export {
invalid_default_snippet,
validate_dynamic_element_tag,
validate_store,
- validate_void_dynamic_element
+ validate_void_dynamic_element,
+ prevent_snippet_stringification
} from '../shared/validate.js';
export { strict_equals, equals } from './dev/equality.js';
export { log_if_contains_state } from './dev/console-log.js';
diff --git a/packages/svelte/src/internal/client/proxy.js b/packages/svelte/src/internal/client/proxy.js
index 6cbd6394df..d690790e3a 100644
--- a/packages/svelte/src/internal/client/proxy.js
+++ b/packages/svelte/src/internal/client/proxy.js
@@ -1,6 +1,6 @@
-/** @import { ProxyMetadata, ProxyStateObject, Source } from '#client' */
+/** @import { Source } from '#client' */
import { DEV } from 'esm-env';
-import { get, component_context, active_effect } from './runtime.js';
+import { get, active_effect, active_reaction, set_active_reaction } from './runtime.js';
import {
array_prototype,
get_descriptor,
@@ -8,9 +8,8 @@ import {
is_array,
object_prototype
} from '../shared/utils.js';
-import { check_ownership, widen_ownership } from './dev/ownership.js';
-import { source, set } from './reactivity/sources.js';
-import { STATE_SYMBOL, STATE_SYMBOL_METADATA } from './constants.js';
+import { state as source, set } from './reactivity/sources.js';
+import { STATE_SYMBOL } from '#client/constants';
import { UNINITIALIZED } from '../../constants.js';
import * as e from './errors.js';
import { get_stack } from './dev/tracing.js';
@@ -19,16 +18,9 @@ import { tracing_mode_flag } from '../flags/index.js';
/**
* @template T
* @param {T} value
- * @param {ProxyMetadata | null} [parent]
- * @param {Source} [prev] dev mode only
* @returns {T}
*/
-export function proxy(value, parent = null, prev) {
- /** @type {Error | null} */
- var stack = null;
- if (DEV && tracing_mode_flag) {
- stack = get_stack('CreatedAt');
- }
+export function proxy(value) {
// if non-proxyable, or is already a proxy, return `value`
if (typeof value !== 'object' || value === null || STATE_SYMBOL in value) {
return value;
@@ -45,37 +37,30 @@ export function proxy(value, parent = null, prev) {
var is_proxied_array = is_array(value);
var version = source(0);
+ var stack = DEV && tracing_mode_flag ? get_stack('CreatedAt') : null;
+ var reaction = active_reaction;
+
+ /**
+ * @template T
+ * @param {() => T} fn
+ */
+ var with_parent = (fn) => {
+ var previous_reaction = active_reaction;
+ set_active_reaction(reaction);
+
+ /** @type {T} */
+ var result = fn();
+
+ set_active_reaction(previous_reaction);
+ return result;
+ };
+
if (is_proxied_array) {
// We need to create the length source eagerly to ensure that
// mutations to the array are properly synced with our proxy
sources.set('length', source(/** @type {any[]} */ (value).length, stack));
}
- /** @type {ProxyMetadata} */
- var metadata;
-
- if (DEV) {
- metadata = {
- parent,
- owners: null
- };
-
- if (prev) {
- // Reuse owners from previous state; necessary because reassignment is not guaranteed to have correct component context.
- // If no previous proxy exists we play it safe and assume ownerless state
- // @ts-expect-error
- const prev_owners = prev.v?.[STATE_SYMBOL_METADATA]?.owners;
- metadata.owners = prev_owners ? new Set(prev_owners) : null;
- } else {
- metadata.owners =
- parent === null
- ? component_context !== null
- ? new Set([component_context.function])
- : null
- : new Set();
- }
- }
-
return new Proxy(/** @type {any} */ (value), {
defineProperty(_, prop, descriptor) {
if (
@@ -94,10 +79,13 @@ export function proxy(value, parent = null, prev) {
var s = sources.get(prop);
if (s === undefined) {
- s = source(descriptor.value, stack);
+ s = with_parent(() => source(descriptor.value, stack));
sources.set(prop, s);
} else {
- set(s, proxy(descriptor.value, metadata));
+ set(
+ s,
+ with_parent(() => proxy(descriptor.value))
+ );
}
return true;
@@ -108,7 +96,10 @@ export function proxy(value, parent = null, prev) {
if (s === undefined) {
if (prop in target) {
- sources.set(prop, source(UNINITIALIZED, stack));
+ sources.set(
+ prop,
+ with_parent(() => source(UNINITIALIZED, stack))
+ );
}
} else {
// When working with arrays, we need to also ensure we update the length when removing
@@ -129,10 +120,6 @@ export function proxy(value, parent = null, prev) {
},
get(target, prop, receiver) {
- if (DEV && prop === STATE_SYMBOL_METADATA) {
- return metadata;
- }
-
if (prop === STATE_SYMBOL) {
return value;
}
@@ -142,28 +129,12 @@ export function proxy(value, parent = null, prev) {
// create a source, but only if it's an own property and not a prototype property
if (s === undefined && (!exists || get_descriptor(target, prop)?.writable)) {
- s = source(proxy(exists ? target[prop] : UNINITIALIZED, metadata), stack);
+ s = with_parent(() => source(proxy(exists ? target[prop] : UNINITIALIZED), stack));
sources.set(prop, s);
}
if (s !== undefined) {
var v = get(s);
-
- // In case of something like `foo = bar.map(...)`, foo would have ownership
- // of the array itself, while the individual items would have ownership
- // of the component that created bar. That means if we later do `foo[0].baz = 42`,
- // we could get a false-positive ownership violation, since the two proxies
- // are not connected to each other via the parent metadata relationship.
- // For this reason, we need to widen the ownership of the children
- // upon access when we detect they are not connected.
- if (DEV) {
- /** @type {ProxyMetadata | undefined} */
- var prop_metadata = v?.[STATE_SYMBOL_METADATA];
- if (prop_metadata && prop_metadata?.parent !== metadata) {
- widen_ownership(metadata, prop_metadata);
- }
- }
-
return v === UNINITIALIZED ? undefined : v;
}
@@ -194,10 +165,6 @@ export function proxy(value, parent = null, prev) {
},
has(target, prop) {
- if (DEV && prop === STATE_SYMBOL_METADATA) {
- return true;
- }
-
if (prop === STATE_SYMBOL) {
return true;
}
@@ -210,7 +177,7 @@ export function proxy(value, parent = null, prev) {
(active_effect !== null && (!has || get_descriptor(target, prop)?.writable))
) {
if (s === undefined) {
- s = source(has ? proxy(target[prop], metadata) : UNINITIALIZED, stack);
+ s = with_parent(() => source(has ? proxy(target[prop]) : UNINITIALIZED, stack));
sources.set(prop, s);
}
@@ -237,7 +204,7 @@ export function proxy(value, parent = null, prev) {
// If the item exists in the original, we need to create a uninitialized source,
// else a later read of the property would result in a source being created with
// the value of the original item at that index.
- other_s = source(UNINITIALIZED, stack);
+ other_s = with_parent(() => source(UNINITIALIZED, stack));
sources.set(i + '', other_s);
}
}
@@ -249,22 +216,19 @@ export function proxy(value, parent = null, prev) {
// object property before writing to that property.
if (s === undefined) {
if (!has || get_descriptor(target, prop)?.writable) {
- s = source(undefined, stack);
- set(s, proxy(value, metadata));
+ s = with_parent(() => source(undefined, stack));
+ set(
+ s,
+ with_parent(() => proxy(value))
+ );
sources.set(prop, s);
}
} else {
has = s.v !== UNINITIALIZED;
- set(s, proxy(value, metadata));
- }
-
- if (DEV) {
- /** @type {ProxyMetadata | undefined} */
- var prop_metadata = value?.[STATE_SYMBOL_METADATA];
- if (prop_metadata && prop_metadata?.parent !== metadata) {
- widen_ownership(metadata, prop_metadata);
- }
- check_ownership(metadata);
+ set(
+ s,
+ with_parent(() => proxy(value))
+ );
}
var descriptor = Reflect.getOwnPropertyDescriptor(target, prop);
@@ -329,8 +293,18 @@ function update_version(signal, d = 1) {
* @param {any} value
*/
export function get_proxied_value(value) {
- if (value !== null && typeof value === 'object' && STATE_SYMBOL in value) {
- return value[STATE_SYMBOL];
+ try {
+ if (value !== null && typeof value === 'object' && STATE_SYMBOL in value) {
+ return value[STATE_SYMBOL];
+ }
+ } catch {
+ // the above if check can throw an error if the value in question
+ // is the contentWindow of an iframe on another domain, in which
+ // case we want to just return the value (because it's definitely
+ // not a proxied value) so we don't break any JavaScript interacting
+ // with that iframe (such as various payment companies client side
+ // JavaScript libraries interacting with their iframes on the same
+ // domain)
}
return value;
diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js
index 7ec1ed30bd..21780be862 100644
--- a/packages/svelte/src/internal/client/reactivity/deriveds.js
+++ b/packages/svelte/src/internal/client/reactivity/deriveds.js
@@ -1,24 +1,15 @@
/** @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 '#client/constants';
import {
active_reaction,
active_effect,
- remove_reactions,
set_signal_status,
skip_reaction,
update_reaction,
increment_write_version,
set_active_effect,
- component_context
+ push_reaction_value
} from '../runtime.js';
import { equals, safe_equals } from './equality.js';
import * as e from '../errors.js';
@@ -26,6 +17,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 +27,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 +40,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,13 +59,23 @@ export function derived(fn) {
signal.created = get_stack('CreatedAt');
}
- if (parent_derived !== null) {
- (parent_derived.children ??= []).push(signal);
- }
-
return signal;
}
+/**
+ * @template V
+ * @param {() => V} fn
+ * @returns {Derived}
+ */
+/*#__NO_SIDE_EFFECTS__*/
+export function user_derived(fn) {
+ const d = derived(fn);
+
+ push_reaction_value(d);
+
+ return d;
+}
+
/**
* @template V
* @param {() => V} fn
@@ -91,19 +92,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 +147,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 +156,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 +182,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 16f076edde..36be1ecd04 100644
--- a/packages/svelte/src/internal/client/reactivity/effects.js
+++ b/packages/svelte/src/internal/client/reactivity/effects.js
@@ -1,22 +1,18 @@
/** @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,
- is_flushing_effect,
remove_reactions,
schedule_effect,
set_active_reaction,
set_is_destroying_effect,
- set_is_flushing_effect,
set_signal_status,
untrack,
- skip_reaction
+ untracking
} from '../runtime.js';
import {
DIRTY,
@@ -35,14 +31,16 @@ import {
INSPECT_EFFECT,
HEAD_EFFECT,
MAYBE_DIRTY,
- EFFECT_HAS_DERIVED
-} from '../constants.js';
+ EFFECT_HAS_DERIVED,
+ BOUNDARY_EFFECT
+} from '#client/constants';
import { set } from './sources.js';
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 { 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
@@ -52,7 +50,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();
}
@@ -84,13 +82,12 @@ function push_effect(effect, parent_effect) {
* @returns {Effect}
*/
function create_effect(type, fn, sync, push = true) {
- var is_root = (type & ROOT_EFFECT) !== 0;
- var parent_effect = active_effect;
+ var parent = active_effect;
if (DEV) {
// Ensure the parent is never an inspect effect
- while (parent_effect !== null && (parent_effect.f & INSPECT_EFFECT) !== 0) {
- parent_effect = parent_effect.parent;
+ while (parent !== null && (parent.f & INSPECT_EFFECT) !== 0) {
+ parent = parent.parent;
}
}
@@ -98,7 +95,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,
@@ -106,7 +102,7 @@ function create_effect(type, fn, sync, push = true) {
fn,
last: null,
next: null,
- parent: is_root ? null : parent_effect,
+ parent,
prev: null,
teardown: null,
transitions: null,
@@ -118,17 +114,12 @@ function create_effect(type, fn, sync, push = true) {
}
if (sync) {
- var previously_flushing_effect = is_flushing_effect;
-
try {
- set_is_flushing_effect(true);
update_effect(effect);
effect.f |= EFFECT_RAN;
} catch (e) {
destroy_effect(effect);
throw e;
- } finally {
- set_is_flushing_effect(previously_flushing_effect);
}
} else if (fn !== null) {
schedule_effect(effect);
@@ -142,17 +133,17 @@ function create_effect(type, fn, sync, push = true) {
effect.first === null &&
effect.nodes_start === null &&
effect.teardown === null &&
- (effect.f & EFFECT_HAS_DERIVED) === 0;
+ (effect.f & (EFFECT_HAS_DERIVED | BOUNDARY_EFFECT)) === 0;
- if (!inert && !is_root && push) {
- if (parent_effect !== null) {
- push_effect(effect, parent_effect);
+ if (!inert && push) {
+ if (parent !== null) {
+ push_effect(effect, parent);
}
// 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);
}
}
@@ -164,13 +155,7 @@ function create_effect(type, fn, sync, push = true) {
* @returns {boolean}
*/
export function effect_tracking() {
- if (active_reaction === null) {
- 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;
}
/**
@@ -342,16 +327,21 @@ export function render_effect(fn) {
}
/**
- * @param {() => void | (() => void)} fn
+ * @param {(...expressions: any) => void | (() => void)} fn
+ * @param {Array<() => any>} thunks
* @returns {Effect}
*/
-export function template_effect(fn) {
+export function template_effect(fn, thunks = [], d = derived) {
+ const deriveds = thunks.map(d);
+ const effect = () => fn(...deriveds.map(get));
+
if (DEV) {
- define_property(fn, 'name', {
+ define_property(effect, 'name', {
value: '{expression}'
});
}
- return block(fn);
+
+ return block(effect);
}
/**
@@ -389,22 +379,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
@@ -416,7 +390,14 @@ export function destroy_effect_children(signal, remove_dom = false) {
while (effect !== null) {
var next = effect.next;
- destroy_effect(effect, remove_dom);
+
+ if ((effect.f & ROOT_EFFECT) !== 0) {
+ // this is now an independent root
+ effect.parent = null;
+ } else {
+ destroy_effect(effect, remove_dom);
+ }
+
effect = next;
}
}
@@ -446,23 +427,11 @@ export function destroy_effect(effect, remove_dom = true) {
var removed = false;
if ((remove_dom || (effect.f & HEAD_EFFECT) !== 0) && effect.nodes_start !== null) {
- /** @type {TemplateNode | null} */
- var node = effect.nodes_start;
- var end = effect.nodes_end;
-
- while (node !== null) {
- /** @type {TemplateNode | null} */
- var next = node === end ? null : /** @type {TemplateNode} */ (get_next_sibling(node));
-
- node.remove();
- node = next;
- }
-
+ remove_effect_dom(effect.nodes_start, /** @type {TemplateNode} */ (effect.nodes_end));
removed = true;
}
destroy_effect_children(effect, remove_dom && !removed);
- destroy_effect_deriveds(effect);
remove_reactions(effect, 0);
set_signal_status(effect, DESTROYED);
@@ -500,6 +469,21 @@ export function destroy_effect(effect, remove_dom = true) {
null;
}
+/**
+ *
+ * @param {TemplateNode | null} node
+ * @param {TemplateNode} end
+ */
+export function remove_effect_dom(node, end) {
+ while (node !== null) {
+ /** @type {TemplateNode | null} */
+ var next = node === end ? null : /** @type {TemplateNode} */ (get_next_sibling(node));
+
+ node.remove();
+ node = next;
+ }
+}
+
/**
* Detach an effect from the effect tree, freeing up memory and
* reducing the amount of work that happens on subsequent traversals
@@ -601,17 +585,21 @@ export function resume_effect(effect) {
*/
function resume_children(effect, local) {
if ((effect.f & INERT) === 0) return;
+ effect.f ^= INERT;
+
+ // Ensure the effect is marked as clean again so that any dirty child
+ // effects can schedule themselves for execution
+ if ((effect.f & CLEAN) === 0) {
+ effect.f ^= CLEAN;
+ }
// If a dependency of this effect changed while it was paused,
- // apply the change now
+ // schedule the effect to update
if (check_dirtiness(effect)) {
- update_effect(effect);
+ set_signal_status(effect, DIRTY);
+ schedule_effect(effect);
}
- // Ensure we toggle the flag after possibly updating the effect so that
- // each block logic can correctly operate on inert items
- effect.f ^= INERT;
-
var child = effect.first;
while (child !== null) {
diff --git a/packages/svelte/src/internal/client/reactivity/equality.js b/packages/svelte/src/internal/client/reactivity/equality.js
index 37a9994ab8..1041238573 100644
--- a/packages/svelte/src/internal/client/reactivity/equality.js
+++ b/packages/svelte/src/internal/client/reactivity/equality.js
@@ -1,4 +1,5 @@
/** @import { Equals } from '#client' */
+
/** @type {Equals} */
export function equals(value) {
return value === this.v;
diff --git a/packages/svelte/src/internal/client/reactivity/props.js b/packages/svelte/src/internal/client/reactivity/props.js
index 3e5a0258c7..8bfd8f9e25 100644
--- a/packages/svelte/src/internal/client/reactivity/props.js
+++ b/packages/svelte/src/internal/client/reactivity/props.js
@@ -1,4 +1,4 @@
-/** @import { Source } from './types.js' */
+/** @import { Derived, Source } from './types.js' */
import { DEV } from 'esm-env';
import {
PROPS_IS_BINDABLE,
@@ -8,25 +8,12 @@ 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,
- get,
- captured_signals,
- set_active_effect,
- untrack,
- update
-} from '../runtime.js';
+import { get, captured_signals, untrack } from '../runtime.js';
import { safe_equals } from './equality.js';
import * as e from '../errors.js';
-import {
- BRANCH_EFFECT,
- LEGACY_DERIVED_PROP,
- LEGACY_PROPS,
- ROOT_EFFECT,
- STATE_SYMBOL
-} from '../constants.js';
+import { LEGACY_DERIVED_PROP, LEGACY_PROPS, STATE_SYMBOL } from '#client/constants';
import { proxy } from '../proxy.js';
import { capture_store_binding } from './store.js';
import { legacy_mode_flag } from '../../flags/index.js';
@@ -249,23 +236,11 @@ export function spread_props(...props) {
}
/**
- * @template T
- * @param {() => T} fn
- * @returns {T}
+ * @param {Derived} current_value
+ * @returns {boolean}
*/
-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);
- }
+function has_destroyed_component_ctx(current_value) {
+ return current_value.ctx?.d ?? false;
}
/**
@@ -342,8 +317,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 +362,24 @@ 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);
+ });
+
+ // Ensure we eagerly capture the initial value if it's bindable
+ if (bindable) {
+ get(current_value);
+ }
if (!immutable) current_value.equals = safe_equals;
@@ -429,11 +407,21 @@ export function prop(props, key, flags, fallback) {
if (fallback_used && fallback_value !== undefined) {
fallback_value = new_value;
}
+
+ if (has_destroyed_component_ctx(current_value)) {
+ return value;
+ }
+
untrack(() => get(current_value)); // force a synchronisation immediately
}
return value;
}
+
+ if (has_destroyed_component_ctx(current_value)) {
+ return current_value.v;
+ }
+
return get(current_value);
};
}
diff --git a/packages/svelte/src/internal/client/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js
index e0facf3b55..9d2ad2baee 100644
--- a/packages/svelte/src/internal/client/reactivity/sources.js
+++ b/packages/svelte/src/internal/client/reactivity/sources.js
@@ -1,24 +1,21 @@
-/** @import { Derived, Effect, Reaction, Source, Value } from '#client' */
+/** @import { Derived, Effect, Source, Value } from '#client' */
import { DEV } from 'esm-env';
import {
- component_context,
active_reaction,
- new_deps,
active_effect,
untracked_writes,
get,
- is_runes,
schedule_effect,
set_untracked_writes,
set_signal_status,
untrack,
increment_write_version,
update_effect,
- derived_sources,
- set_derived_sources,
+ reaction_sources,
check_dirtiness,
- set_is_flushing_effect,
- is_flushing_effect
+ untracking,
+ is_destroying_effect,
+ push_reaction_value
} from '../runtime.js';
import { equals, safe_equals } from './equality.js';
import {
@@ -29,13 +26,18 @@ import {
INSPECT_EFFECT,
UNOWNED,
MAYBE_DIRTY,
- BLOCK_EFFECT
-} from '../constants.js';
+ BLOCK_EFFECT,
+ ROOT_EFFECT
+} from '#client/constants';
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 { component_context, is_runes } from '../context.js';
+import { proxy } from '../proxy.js';
+import { execute_derived } from './deriveds.js';
export let inspect_effects = new Set();
+export const old_values = new Map();
/**
* @param {Set} v
@@ -50,6 +52,7 @@ export function set_inspect_effects(v) {
* @param {Error | null} [stack]
* @returns {Source}
*/
+// TODO rename this to `state` throughout the codebase
export function source(v, stack) {
/** @type {Value} */
var signal = {
@@ -72,9 +75,15 @@ export function source(v, stack) {
/**
* @template V
* @param {V} v
+ * @param {Error | null} [stack]
*/
-export function state(v) {
- return push_derived_source(source(v));
+/*#__NO_SIDE_EFFECTS__*/
+export function state(v, stack) {
+ const s = source(v, stack);
+
+ push_reaction_value(s);
+
+ return s;
}
/**
@@ -99,33 +108,6 @@ export function mutable_source(initial_value, immutable = false) {
return s;
}
-/**
- * @template V
- * @param {V} v
- * @param {boolean} [immutable]
- * @returns {Source}
- */
-export function mutable_state(v, immutable = false) {
- return push_derived_source(mutable_source(v, immutable));
-}
-
-/**
- * @template V
- * @param {Source} source
- */
-/*#__NO_SIDE_EFFECTS__*/
-function push_derived_source(source) {
- if (active_reaction !== null && (active_reaction.f & DERIVED) !== 0) {
- if (derived_sources === null) {
- set_derived_sources([source]);
- } else {
- derived_sources.push(source);
- }
- }
-
- return source;
-}
-
/**
* @template V
* @param {Value} source
@@ -143,21 +125,23 @@ export function mutate(source, value) {
* @template V
* @param {Source} source
* @param {V} value
+ * @param {boolean} [should_proxy]
* @returns {V}
*/
-export function set(source, value) {
+export function set(source, value, should_proxy = false) {
if (
active_reaction !== null &&
+ !untracking &&
is_runes() &&
(active_reaction.f & (DERIVED | BLOCK_EFFECT)) !== 0 &&
- // If the source was created locally within the current derived, then
- // we allow the mutation.
- (derived_sources === null || !derived_sources.includes(source))
+ !reaction_sources?.includes(source)
) {
e.state_unsafe_mutation();
}
- return internal_set(source, value);
+ let new_value = should_proxy ? proxy(value) : value;
+
+ return internal_set(source, new_value);
}
/**
@@ -169,8 +153,14 @@ export function set(source, value) {
export function internal_set(source, value) {
if (!source.equals(value)) {
var old_value = source.v;
+
+ if (is_destroying_effect) {
+ old_values.set(source, value);
+ } else {
+ old_values.set(source, old_value);
+ }
+
source.v = value;
- source.wv = increment_write_version();
if (DEV && tracing_mode_flag) {
source.updated = get_stack('UpdatedAt');
@@ -180,49 +170,49 @@ export function internal_set(source, value) {
}
}
+ if ((source.f & DERIVED) !== 0) {
+ // if we are assigning to a dirty derived we set it to clean/maybe dirty but we also eagerly execute it to track the dependencies
+ if ((source.f & DIRTY) !== 0) {
+ execute_derived(/** @type {Derived} */ (source));
+ }
+ set_signal_status(source, (source.f & UNOWNED) === 0 ? CLEAN : MAYBE_DIRTY);
+ }
+
+ source.wv = increment_write_version();
+
mark_reactions(source, DIRTY);
- // If the current signal is running for the first time, it won't have any
- // reactions as we only allocate and assign the reactions after the signal
- // has fully executed. So in the case of ensuring it registers the reaction
+ // It's possible that the current reaction might not have up-to-date dependencies
+ // whilst it's actively running. So in the case of ensuring it registers the reaction
// properly for itself, we need to ensure the current effect actually gets
// scheduled. i.e: `$effect(() => x++)`
if (
is_runes() &&
active_effect !== null &&
(active_effect.f & CLEAN) !== 0 &&
- (active_effect.f & BRANCH_EFFECT) === 0
+ (active_effect.f & (BRANCH_EFFECT | ROOT_EFFECT)) === 0
) {
- if (new_deps !== null && new_deps.includes(source)) {
- set_signal_status(active_effect, DIRTY);
- schedule_effect(active_effect);
+ if (untracked_writes === null) {
+ set_untracked_writes([source]);
} else {
- if (untracked_writes === null) {
- set_untracked_writes([source]);
- } else {
- untracked_writes.push(source);
- }
+ untracked_writes.push(source);
}
}
if (DEV && inspect_effects.size > 0) {
const inspects = Array.from(inspect_effects);
- var previously_flushing_effect = is_flushing_effect;
- set_is_flushing_effect(true);
- try {
- for (const effect of inspects) {
- // Mark clean inspect-effects as maybe dirty and then check their dirtiness
- // instead of just updating the effects - this way we avoid overfiring.
- if ((effect.f & CLEAN) !== 0) {
- set_signal_status(effect, MAYBE_DIRTY);
- }
- if (check_dirtiness(effect)) {
- update_effect(effect);
- }
+
+ for (const effect of inspects) {
+ // Mark clean inspect-effects as maybe dirty and then check their dirtiness
+ // instead of just updating the effects - this way we avoid overfiring.
+ if ((effect.f & CLEAN) !== 0) {
+ set_signal_status(effect, MAYBE_DIRTY);
+ }
+ if (check_dirtiness(effect)) {
+ update_effect(effect);
}
- } finally {
- set_is_flushing_effect(previously_flushing_effect);
}
+
inspect_effects.clear();
}
}
@@ -230,6 +220,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 3a76a3ff83..5ef0097649 100644
--- a/packages/svelte/src/internal/client/reactivity/types.d.ts
+++ b/packages/svelte/src/internal/client/reactivity/types.d.ts
@@ -36,8 +36,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;
}
@@ -51,8 +51,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 3c8879eb31..7595aa4a19 100644
--- a/packages/svelte/src/internal/client/runtime.js
+++ b/packages/svelte/src/internal/client/runtime.js
@@ -4,14 +4,11 @@ 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';
import {
EFFECT,
- RENDER_EFFECT,
DIRTY,
MAYBE_DIRTY,
CLEAN,
@@ -25,40 +22,38 @@ import {
ROOT_EFFECT,
LEGACY_DERIVED_PROP,
DISCONNECTED,
- BOUNDARY_EFFECT
+ BOUNDARY_EFFECT,
+ EFFECT_IS_UPDATING
} 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, old_values } 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;
// Used for DEV time error handling
/** @param {WeakSet} value */
const handled_errors = new WeakSet();
-export let is_throwing_error = false;
+let is_throwing_error = false;
-// Used for controlling the flush of effects.
-let scheduler_mode = FLUSH_MICROTASK;
-// Used for handling scheduling
-let is_micro_task_queued = false;
+let is_flushing = false;
/** @type {Effect | null} */
let last_scheduled_effect = null;
-export let is_flushing_effect = false;
-export let is_destroying_effect = false;
+let is_updating_effect = false;
-/** @param {boolean} value */
-export function set_is_flushing_effect(value) {
- is_flushing_effect = value;
-}
+export let is_destroying_effect = false;
/** @param {boolean} value */
export function set_is_destroying_effect(value) {
@@ -70,7 +65,6 @@ export function set_is_destroying_effect(value) {
/** @type {Effect[]} */
let queued_root_effects = [];
-let flush_count = 0;
/** @type {Effect[]} Stack of effects, dev only */
let dev_effect_stack = [];
// Handle signal reactivity tree dependencies and reactions
@@ -78,6 +72,8 @@ let dev_effect_stack = [];
/** @type {null | Reaction} */
export let active_reaction = null;
+export let untracking = false;
+
/** @param {null | Reaction} reaction */
export function set_active_reaction(reaction) {
active_reaction = reaction;
@@ -92,17 +88,21 @@ export function set_active_effect(effect) {
}
/**
- * When sources are created within a derived, we record them so that we can safely allow
- * local mutations to these sources without the side-effect error being invoked unnecessarily.
+ * When sources are created within a reaction, reading and writing
+ * them should not cause a re-run
* @type {null | Source[]}
*/
-export let derived_sources = null;
+export let reaction_sources = null;
-/**
- * @param {Source[] | null} sources
- */
-export function set_derived_sources(sources) {
- derived_sources = sources;
+/** @param {Value} value */
+export function push_reaction_value(value) {
+ if (active_reaction !== null && active_reaction.f & EFFECT_IS_UPDATING) {
+ if (reaction_sources === null) {
+ reaction_sources = [value];
+ } else {
+ reaction_sources.push(value);
+ }
+ }
}
/**
@@ -111,7 +111,7 @@ export function set_derived_sources(sources) {
* and until a new dependency is accessed — we track this via `skipped_deps`
* @type {null | Value[]}
*/
-export let new_deps = null;
+let new_deps = null;
let skipped_deps = 0;
@@ -148,41 +148,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
@@ -210,18 +179,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;
}
}
@@ -349,7 +328,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`
});
@@ -382,6 +361,33 @@ export function handle_error(error, effect, previous_effect, component_context)
}
}
+/**
+ * @param {Value} signal
+ * @param {Effect} effect
+ * @param {boolean} [root]
+ */
+function schedule_possible_effect_self_invalidation(signal, effect, root = true) {
+ var reactions = signal.reactions;
+ if (reactions === null) return;
+
+ for (var i = 0; i < reactions.length; i++) {
+ var reaction = reactions[i];
+
+ if (reaction_sources?.includes(signal)) continue;
+
+ if ((reaction.f & DERIVED) !== 0) {
+ schedule_possible_effect_self_invalidation(/** @type {Derived} */ (reaction), effect, false);
+ } else if (effect === reaction) {
+ if (root) {
+ set_signal_status(reaction, DIRTY);
+ } else if ((reaction.f & CLEAN) !== 0) {
+ set_signal_status(reaction, MAYBE_DIRTY);
+ }
+ schedule_effect(/** @type {Effect} */ (reaction));
+ }
+ }
+}
+
/**
* @template V
* @param {Reaction} reaction
@@ -393,19 +399,26 @@ export function update_reaction(reaction) {
var previous_untracked_writes = untracked_writes;
var previous_reaction = active_reaction;
var previous_skip_reaction = skip_reaction;
- var prev_derived_sources = derived_sources;
+ var previous_reaction_sources = reaction_sources;
var previous_component_context = component_context;
+ var previous_untracking = untracking;
+
var flags = reaction.f;
new_deps = /** @type {null | Value[]} */ (null);
skipped_deps = 0;
untracked_writes = null;
+ skip_reaction =
+ (flags & UNOWNED) !== 0 && (untracking || !is_updating_effect || active_reaction === null);
active_reaction = (flags & (BRANCH_EFFECT | ROOT_EFFECT)) === 0 ? reaction : null;
- skip_reaction = !is_flushing_effect && (flags & UNOWNED) !== 0;
- derived_sources = null;
- component_context = reaction.ctx;
+
+ reaction_sources = null;
+ set_component_context(reaction.ctx);
+ untracking = false;
read_version++;
+ reaction.f |= EFFECT_IS_UPDATING;
+
try {
var result = /** @type {Function} */ (0, reaction.fn)();
var deps = reaction.deps;
@@ -434,12 +447,38 @@ export function update_reaction(reaction) {
deps.length = skipped_deps;
}
+ // If we're inside an effect and we have untracked writes, then we need to
+ // ensure that if any of those untracked writes result in re-invalidation
+ // of the current effect, then that happens accordingly
+ if (
+ is_runes() &&
+ untracked_writes !== null &&
+ !untracking &&
+ deps !== null &&
+ (reaction.f & (DERIVED | MAYBE_DIRTY | DIRTY)) === 0
+ ) {
+ for (i = 0; i < /** @type {Source[]} */ (untracked_writes).length; i++) {
+ schedule_possible_effect_self_invalidation(
+ untracked_writes[i],
+ /** @type {Effect} */ (reaction)
+ );
+ }
+ }
+
// If we are returning to an previous reaction then
// we need to increment the read version to ensure that
// any dependencies in this reaction aren't marked with
// the same version
- if (previous_reaction !== null) {
+ if (previous_reaction !== null && previous_reaction !== reaction) {
read_version++;
+
+ if (untracked_writes !== null) {
+ if (previous_untracked_writes === null) {
+ previous_untracked_writes = untracked_writes;
+ } else {
+ previous_untracked_writes.push(.../** @type {Source[]} */ (untracked_writes));
+ }
+ }
}
return result;
@@ -449,8 +488,11 @@ export function update_reaction(reaction) {
untracked_writes = previous_untracked_writes;
active_reaction = previous_reaction;
skip_reaction = previous_skip_reaction;
- derived_sources = prev_derived_sources;
- component_context = previous_component_context;
+ reaction_sources = previous_reaction_sources;
+ set_component_context(previous_component_context);
+ untracking = previous_untracking;
+
+ reaction.f ^= EFFECT_IS_UPDATING;
}
}
@@ -491,6 +533,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);
}
}
@@ -524,12 +568,14 @@ export function update_effect(effect) {
var previous_effect = active_effect;
var previous_component_context = component_context;
+ var was_updating_effect = is_updating_effect;
active_effect = effect;
+ is_updating_effect = true;
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 {
@@ -538,7 +584,6 @@ export function update_effect(effect) {
} else {
destroy_effect_children(effect);
}
- destroy_effect_deriveds(effect);
execute_effect_teardown(effect);
var teardown = update_reaction(effect);
@@ -568,10 +613,11 @@ export function update_effect(effect) {
} catch (error) {
handle_error(error, effect, previous_effect, previous_component_context || effect.ctx);
} finally {
+ is_updating_effect = was_updating_effect;
active_effect = previous_effect;
if (DEV) {
- dev_current_component_function = previous_component_fn;
+ set_dev_current_component_function(previous_component_fn);
}
}
}
@@ -586,72 +632,69 @@ function log_effect_stack() {
}
function infinite_loop_guard() {
- if (flush_count > 1000) {
- flush_count = 0;
- try {
- e.effect_update_depth_exceeded();
- } catch (error) {
+ try {
+ e.effect_update_depth_exceeded();
+ } catch (error) {
+ if (DEV) {
+ // stack is garbage, ignore. Instead add a console.error message.
+ define_property(error, 'stack', {
+ value: ''
+ });
+ }
+ // Try and handle the error so it can be caught at a boundary, that's
+ // if there's an effect available from when it was last scheduled
+ if (last_scheduled_effect !== null) {
if (DEV) {
- // stack is garbage, ignore. Instead add a console.error message.
- define_property(error, 'stack', {
- value: ''
- });
- }
- // Try and handle the error so it can be caught at a boundary, that's
- // if there's an effect available from when it was last scheduled
- if (last_scheduled_effect !== null) {
- if (DEV) {
- try {
- handle_error(error, last_scheduled_effect, null, null);
- } catch (e) {
- // Only log the effect stack if the error is re-thrown
- log_effect_stack();
- throw e;
- }
- } else {
+ try {
handle_error(error, last_scheduled_effect, null, null);
- }
- } else {
- if (DEV) {
+ } catch (e) {
+ // Only log the effect stack if the error is re-thrown
log_effect_stack();
+ throw e;
}
- throw error;
+ } else {
+ handle_error(error, last_scheduled_effect, null, null);
}
+ } else {
+ if (DEV) {
+ log_effect_stack();
+ }
+ throw error;
}
}
- flush_count++;
}
-/**
- * @param {Array} root_effects
- * @returns {void}
- */
-function flush_queued_root_effects(root_effects) {
- var length = root_effects.length;
- if (length === 0) {
- return;
- }
- infinite_loop_guard();
-
- var previously_flushing_effect = is_flushing_effect;
- is_flushing_effect = true;
+function flush_queued_root_effects() {
+ var was_updating_effect = is_updating_effect;
try {
- for (var i = 0; i < length; i++) {
- var effect = root_effects[i];
+ var flush_count = 0;
+ is_updating_effect = true;
- if ((effect.f & CLEAN) === 0) {
- effect.f ^= CLEAN;
+ while (queued_root_effects.length > 0) {
+ if (flush_count++ > 1000) {
+ infinite_loop_guard();
}
- /** @type {Effect[]} */
- var collected_effects = [];
+ var root_effects = queued_root_effects;
+ var length = root_effects.length;
- process_effects(effect, collected_effects);
- flush_queued_effects(collected_effects);
+ queued_root_effects = [];
+
+ for (var i = 0; i < length; i++) {
+ var collected_effects = process_effects(root_effects[i]);
+ flush_queued_effects(collected_effects);
+ }
+ old_values.clear();
}
} finally {
- is_flushing_effect = previously_flushing_effect;
+ is_flushing = false;
+ is_updating_effect = was_updating_effect;
+
+ last_scheduled_effect = null;
+ if (DEV) {
+ dev_effect_stack = [];
+ }
}
}
@@ -693,39 +736,17 @@ function flush_queued_effects(effects) {
}
}
-function process_deferred() {
- is_micro_task_queued = false;
- if (flush_count > 1001) {
- return;
- }
- const previous_queued_root_effects = queued_root_effects;
- queued_root_effects = [];
- flush_queued_root_effects(previous_queued_root_effects);
-
- if (!is_micro_task_queued) {
- flush_count = 0;
- last_scheduled_effect = null;
- if (DEV) {
- dev_effect_stack = [];
- }
- }
-}
-
/**
* @param {Effect} signal
* @returns {void}
*/
export function schedule_effect(signal) {
- if (scheduler_mode === FLUSH_MICROTASK) {
- if (!is_micro_task_queued) {
- is_micro_task_queued = true;
- queueMicrotask(process_deferred);
- }
+ if (!is_flushing) {
+ is_flushing = true;
+ queueMicrotask(flush_queued_root_effects);
}
- last_scheduled_effect = signal;
-
- var effect = signal;
+ var effect = (last_scheduled_effect = signal);
while (effect.parent !== null) {
effect = effect.parent;
@@ -747,113 +768,89 @@ export function schedule_effect(signal) {
* bitwise flag passed in only. The collected effects array will be populated with all the user
* effects to be flushed.
*
- * @param {Effect} effect
- * @param {Effect[]} collected_effects
- * @returns {void}
+ * @param {Effect} root
+ * @returns {Effect[]}
*/
-function process_effects(effect, collected_effects) {
- var current_effect = effect.first;
+function process_effects(root) {
+ /** @type {Effect[]} */
var effects = [];
- main_loop: while (current_effect !== null) {
- var flags = current_effect.f;
- var is_branch = (flags & BRANCH_EFFECT) !== 0;
+ /** @type {Effect | null} */
+ var effect = root;
+
+ while (effect !== null) {
+ var flags = effect.f;
+ var is_branch = (flags & (BRANCH_EFFECT | ROOT_EFFECT)) !== 0;
var is_skippable_branch = is_branch && (flags & CLEAN) !== 0;
- 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(effect);
+ } else if (is_branch) {
+ 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 = effect;
+ if (check_dirtiness(effect)) {
+ update_effect(effect);
}
+ } catch (error) {
+ handle_error(error, effect, null, effect.ctx);
+ } finally {
+ active_reaction = previous_active_reaction;
}
+ }
- var child = current_effect.first;
+ /** @type {Effect | null} */
+ var child = effect.first;
- if (child !== null) {
- current_effect = child;
- continue;
- }
- } else if ((flags & EFFECT) !== 0) {
- effects.push(current_effect);
+ if (child !== null) {
+ effect = child;
+ continue;
}
}
- if (sibling === null) {
- let parent = current_effect.parent;
+ var parent = effect.parent;
+ effect = effect.next;
- while (parent !== null) {
- if (effect === parent) {
- break main_loop;
- }
- var parent_sibling = parent.next;
- if (parent_sibling !== null) {
- current_effect = parent_sibling;
- continue main_loop;
- }
- parent = parent.parent;
- }
+ while (effect === null && parent !== null) {
+ effect = parent.next;
+ parent = parent.parent;
}
-
- 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;
}
/**
- * Internal version of `flushSync` with the option to not flush previous effects.
- * Returns the result of the passed function, if given.
- * @param {() => any} [fn]
- * @returns {any}
+ * Synchronously flush any pending updates.
+ * Returns void if no callback is provided, otherwise returns the result of calling the callback.
+ * @template [T=void]
+ * @param {(() => T) | undefined} [fn]
+ * @returns {T}
*/
-export function flush_sync(fn) {
- var previous_scheduler_mode = scheduler_mode;
- var previous_queued_root_effects = queued_root_effects;
-
- try {
- infinite_loop_guard();
-
- /** @type {Effect[]} */
- const root_effects = [];
+export function flushSync(fn) {
+ var result;
- scheduler_mode = FLUSH_SYNC;
- queued_root_effects = root_effects;
- is_micro_task_queued = false;
-
- flush_queued_root_effects(previous_queued_root_effects);
+ if (fn) {
+ is_flushing = true;
+ flush_queued_root_effects();
+ result = fn();
+ }
- var result = fn?.();
+ flush_tasks();
+ while (queued_root_effects.length > 0) {
+ is_flushing = true;
+ flush_queued_root_effects();
flush_tasks();
- if (queued_root_effects.length > 0 || root_effects.length > 0) {
- flush_sync();
- }
-
- flush_count = 0;
- last_scheduled_effect = null;
- if (DEV) {
- dev_effect_stack = [];
- }
-
- return result;
- } finally {
- scheduler_mode = previous_scheduler_mode;
- queued_root_effects = previous_queued_root_effects;
}
+
+ return /** @type {T} */ (result);
}
/**
@@ -862,9 +859,9 @@ export function flush_sync(fn) {
*/
export async function tick() {
await Promise.resolve();
- // By calling flush_sync we guarantee that any pending state changes are applied after one tick.
+ // By calling flushSync we guarantee that any pending state changes are applied after one tick.
// TODO look into whether we can make flushing subsequent updates synchronously in the future.
- flush_sync();
+ flushSync();
}
/**
@@ -876,70 +873,44 @@ 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);
}
// Register the dependency on the current reaction signal.
- if (active_reaction !== null) {
- if (derived_sources !== null && derived_sources.includes(signal)) {
- e.state_unsafe_local_read();
- }
- var deps = active_reaction.deps;
- if (signal.rv < read_version) {
- signal.rv = read_version;
- // If the signal is accessing the same dependencies in the same
- // order as it did last time, increment `skipped_deps`
- // rather than updating `new_deps`, which creates GC cost
- if (new_deps === null && deps !== null && deps[skipped_deps] === signal) {
- skipped_deps++;
- } else if (new_deps === null) {
- new_deps = [signal];
- } else {
- new_deps.push(signal);
- }
-
- if (
- untracked_writes !== null &&
- active_effect !== null &&
- (active_effect.f & CLEAN) !== 0 &&
- (active_effect.f & BRANCH_EFFECT) === 0 &&
- untracked_writes.includes(signal)
- ) {
- set_signal_status(active_effect, DIRTY);
- schedule_effect(active_effect);
+ if (active_reaction !== null && !untracking) {
+ if (!reaction_sources?.includes(signal)) {
+ var deps = active_reaction.deps;
+ if (signal.rv < read_version) {
+ signal.rv = read_version;
+ // If the signal is accessing the same dependencies in the same
+ // order as it did last time, increment `skipped_deps`
+ // rather than updating `new_deps`, which creates GC cost
+ if (new_deps === null && deps !== null && deps[skipped_deps] === signal) {
+ skipped_deps++;
+ } else if (new_deps === null) {
+ new_deps = [signal];
+ } 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;
}
}
@@ -973,6 +944,10 @@ export function get(signal) {
}
}
+ if (is_destroying_effect && old_values.has(signal)) {
+ return old_values.get(signal);
+ }
+
return signal.v;
}
@@ -991,7 +966,7 @@ export function safe_get(signal) {
* @template T
* @param {() => T} fn
*/
-export function capture_signals(fn) {
+function capture_signals(fn) {
var previous_captured_signals = captured_signals;
captured_signals = new Set();
@@ -1052,12 +1027,12 @@ export function invalidate_inner_signals(fn) {
* @returns {T}
*/
export function untrack(fn) {
- const previous_reaction = active_reaction;
+ var previous_untracking = untracking;
try {
- active_reaction = null;
+ untracking = true;
return fn();
} finally {
- active_reaction = previous_reaction;
+ untracking = previous_untracking;
}
}
@@ -1072,138 +1047,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
@@ -1222,78 +1065,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).
@@ -1367,37 +1138,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/types.d.ts b/packages/svelte/src/internal/client/types.d.ts
index 7208ed7783..b46bdf2013 100644
--- a/packages/svelte/src/internal/client/types.d.ts
+++ b/packages/svelte/src/internal/client/types.d.ts
@@ -14,6 +14,8 @@ export type ComponentContext = {
p: null | ComponentContext;
/** context */
c: null | Map;
+ /** destroyed */
+ d: boolean;
/** deferred effects */
e: null | Array<{
fn: () => void | (() => void);
@@ -177,14 +179,6 @@ export type TaskCallback = (now: number) => boolean | void;
export type TaskEntry = { c: TaskCallback; f: () => void };
-/** Dev-only */
-export interface ProxyMetadata {
- /** The components that 'own' this state, if any. `null` means no owners, i.e. everyone can mutate this state. */
- owners: null | Set;
- /** The parent metadata object */
- parent: null | ProxyMetadata;
-}
-
export type ProxyStateObject> = T & {
[STATE_SYMBOL]: T;
};
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/client/warnings.js b/packages/svelte/src/internal/client/warnings.js
index 250c6eca2f..c84b487e28 100644
--- a/packages/svelte/src/internal/client/warnings.js
+++ b/packages/svelte/src/internal/client/warnings.js
@@ -129,27 +129,30 @@ export function lifecycle_double_unmount() {
}
/**
- * %parent% passed a value to %child% with `bind:`, but the value is owned by %owner%. Consider creating a binding between %owner% and %parent%
+ * %parent% passed property `%prop%` to %child% with `bind:`, but its parent component %owner% did not declare `%prop%` as a binding. Consider creating a binding between %owner% and %parent% (e.g. `bind:%prop%={...}` instead of `%prop%={...}`)
* @param {string} parent
+ * @param {string} prop
* @param {string} child
* @param {string} owner
*/
-export function ownership_invalid_binding(parent, child, owner) {
+export function ownership_invalid_binding(parent, prop, child, owner) {
if (DEV) {
- console.warn(`%c[svelte] ownership_invalid_binding\n%c${parent} passed a value to ${child} with \`bind:\`, but the value is owned by ${owner}. Consider creating a binding between ${owner} and ${parent}\nhttps://svelte.dev/e/ownership_invalid_binding`, bold, normal);
+ console.warn(`%c[svelte] ownership_invalid_binding\n%c${parent} passed property \`${prop}\` to ${child} with \`bind:\`, but its parent component ${owner} did not declare \`${prop}\` as a binding. Consider creating a binding between ${owner} and ${parent} (e.g. \`bind:${prop}={...}\` instead of \`${prop}={...}\`)\nhttps://svelte.dev/e/ownership_invalid_binding`, bold, normal);
} else {
console.warn(`https://svelte.dev/e/ownership_invalid_binding`);
}
}
/**
- * %component% mutated a value owned by %owner%. This is strongly discouraged. Consider passing values to child components with `bind:`, or use a callback instead
- * @param {string | undefined | null} [component]
- * @param {string | undefined | null} [owner]
+ * Mutating unbound props (`%name%`, at %location%) is strongly discouraged. Consider using `bind:%prop%={...}` in %parent% (or using a callback) instead
+ * @param {string} name
+ * @param {string} location
+ * @param {string} prop
+ * @param {string} parent
*/
-export function ownership_invalid_mutation(component, owner) {
+export function ownership_invalid_mutation(name, location, prop, parent) {
if (DEV) {
- console.warn(`%c[svelte] ownership_invalid_mutation\n%c${component ? `${component} mutated a value owned by ${owner}. This is strongly discouraged. Consider passing values to child components with \`bind:\`, or use a callback instead` : 'Mutating a value outside the component that created it is strongly discouraged. Consider passing values to child components with `bind:`, or use a callback instead'}\nhttps://svelte.dev/e/ownership_invalid_mutation`, bold, normal);
+ console.warn(`%c[svelte] ownership_invalid_mutation\n%cMutating unbound props (\`${name}\`, at ${location}) is strongly discouraged. Consider using \`bind:${prop}={...}\` in ${parent} (or using a callback) instead\nhttps://svelte.dev/e/ownership_invalid_mutation`, bold, normal);
} else {
console.warn(`https://svelte.dev/e/ownership_invalid_mutation`);
}
diff --git a/packages/svelte/src/internal/disclose-version.js b/packages/svelte/src/internal/disclose-version.js
index 5e72f1e216..86c3482b5d 100644
--- a/packages/svelte/src/internal/disclose-version.js
+++ b/packages/svelte/src/internal/disclose-version.js
@@ -1,5 +1,6 @@
import { PUBLIC_VERSION } from '../version.js';
-if (typeof window !== 'undefined')
- // @ts-ignore
- (window.__svelte ||= { v: new Set() }).v.add(PUBLIC_VERSION);
+if (typeof window !== 'undefined') {
+ // @ts-expect-error
+ ((window.__svelte ??= {}).v ??= new Set()).add(PUBLIC_VERSION);
+}
diff --git a/packages/svelte/src/internal/server/blocks/snippet.js b/packages/svelte/src/internal/server/blocks/snippet.js
index 3c5e860790..9e96ae3430 100644
--- a/packages/svelte/src/internal/server/blocks/snippet.js
+++ b/packages/svelte/src/internal/server/blocks/snippet.js
@@ -1,5 +1,5 @@
/** @import { Snippet } from 'svelte' */
-/** @import { Payload } from '#server' */
+/** @import { Payload } from '../payload' */
/** @import { Getters } from '#shared' */
/**
diff --git a/packages/svelte/src/internal/server/dev.js b/packages/svelte/src/internal/server/dev.js
index ecf4e67429..efc761d7c5 100644
--- a/packages/svelte/src/internal/server/dev.js
+++ b/packages/svelte/src/internal/server/dev.js
@@ -1,10 +1,12 @@
-/** @import { Component, Payload } from '#server' */
+/** @import { Component } from '#server' */
import { FILENAME } from '../../constants.js';
import {
is_tag_valid_with_ancestor,
is_tag_valid_with_parent
} from '../../html-tree-validation.js';
import { current_component } from './context.js';
+import { invalid_snippet_arguments } from '../shared/errors.js';
+import { HeadPayload, Payload } from './payload.js';
/**
* @typedef {{
@@ -24,14 +26,6 @@ let parent = null;
/** @type {Set} */
let seen;
-/**
- * @param {Element} element
- */
-function stringify(element) {
- if (element.filename === null) return `\`<${element.tag}>\``;
- return `\`<${element.tag}>\` (${element.filename}:${element.line}:${element.column})`;
-}
-
/**
* @param {Payload} payload
* @param {string} message
@@ -98,3 +92,16 @@ export function push_element(payload, tag, line, column) {
export function pop_element() {
parent = /** @type {Element} */ (parent).parent;
}
+
+/**
+ * @param {Payload} payload
+ */
+export function validate_snippet_args(payload) {
+ if (
+ typeof payload !== 'object' ||
+ // for some reason typescript consider the type of payload as never after the first instanceof
+ !(payload instanceof Payload || /** @type {any} */ (payload) instanceof HeadPayload)
+ ) {
+ invalid_snippet_arguments();
+ }
+}
diff --git a/packages/svelte/src/internal/server/index.js b/packages/svelte/src/internal/server/index.js
index 89b3c33df8..b58a1d4372 100644
--- a/packages/svelte/src/internal/server/index.js
+++ b/packages/svelte/src/internal/server/index.js
@@ -1,8 +1,8 @@
/** @import { ComponentType, SvelteComponent } from 'svelte' */
-/** @import { Component, Payload, RenderOutput } from '#server' */
+/** @import { Component, RenderOutput } from '#server' */
/** @import { Store } from '#shared' */
export { FILENAME, HMR } from '../../constants.js';
-import { attr, clsx } from '../shared/attributes.js';
+import { attr, clsx, to_class, to_style } from '../shared/attributes.js';
import { is_promise, noop } from '../shared/utils.js';
import { subscribe_to_store } from '../../store/utils.js';
import {
@@ -10,46 +10,20 @@ import {
ELEMENT_PRESERVE_ATTRIBUTE_CASE,
ELEMENT_IS_NAMESPACED
} from '../../constants.js';
-
import { escape_html } from '../../escaping.js';
import { DEV } from 'esm-env';
import { current_component, pop, push } from './context.js';
-import { EMPTY_COMMENT, BLOCK_CLOSE, BLOCK_OPEN } from './hydration.js';
+import { EMPTY_COMMENT, BLOCK_CLOSE, BLOCK_OPEN, BLOCK_OPEN_ELSE } from './hydration.js';
import { validate_store } from '../shared/validate.js';
import { is_boolean_attribute, is_raw_text_element, is_void } from '../../utils.js';
import { reset_elements } from './dev.js';
+import { Payload } from './payload.js';
// https://html.spec.whatwg.org/multipage/syntax.html#attributes-2
// https://infra.spec.whatwg.org/#noncharacter
const INVALID_ATTR_NAME_CHAR_REGEX =
/[\s'">/=\u{FDD0}-\u{FDEF}\u{FFFE}\u{FFFF}\u{1FFFE}\u{1FFFF}\u{2FFFE}\u{2FFFF}\u{3FFFE}\u{3FFFF}\u{4FFFE}\u{4FFFF}\u{5FFFE}\u{5FFFF}\u{6FFFE}\u{6FFFF}\u{7FFFE}\u{7FFFF}\u{8FFFE}\u{8FFFF}\u{9FFFE}\u{9FFFF}\u{AFFFE}\u{AFFFF}\u{BFFFE}\u{BFFFF}\u{CFFFE}\u{CFFFF}\u{DFFFE}\u{DFFFF}\u{EFFFE}\u{EFFFF}\u{FFFFE}\u{FFFFF}\u{10FFFE}\u{10FFFF}]/u;
-/**
- * @param {Payload} to_copy
- * @returns {Payload}
- */
-export function copy_payload({ out, css, head }) {
- return {
- out,
- css: new Set(css),
- head: {
- title: head.title,
- out: head.out
- }
- };
-}
-
-/**
- * Assigns second payload to first
- * @param {Payload} p1
- * @param {Payload} p2
- * @returns {void}
- */
-export function assign_payload(p1, p2) {
- p1.out = p2.out;
- p1.head = p2.head;
-}
-
/**
* @param {Payload} payload
* @param {string} tag
@@ -88,12 +62,11 @@ export let on_destroy = [];
* 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; idPrefix?: string }} [options]
* @returns {RenderOutput}
*/
export function render(component, options = {}) {
- /** @type {Payload} */
- const payload = { out: '', css: new Set(), head: { title: '', out: '' } };
+ const payload = new Payload(options.idPrefix ? options.idPrefix + '-' : '');
const prev_on_destroy = on_destroy;
on_destroy = [];
@@ -183,32 +156,23 @@ export function css_props(payload, is_html, props, component, dynamic = false) {
/**
* @param {Record} attrs
- * @param {Record} [classes]
+ * @param {string | null} css_hash
+ * @param {Record} [classes]
* @param {Record} [styles]
* @param {number} [flags]
* @returns {string}
*/
-export function spread_attributes(attrs, classes, styles, flags = 0) {
+export function spread_attributes(attrs, css_hash, classes, styles, flags = 0) {
if (styles) {
- attrs.style = attrs.style
- ? style_object_to_string(merge_styles(/** @type {string} */ (attrs.style), styles))
- : style_object_to_string(styles);
+ attrs.style = to_style(attrs.style, styles);
}
if (attrs.class) {
attrs.class = clsx(attrs.class);
}
- if (classes) {
- const classlist = attrs.class ? [attrs.class] : [];
-
- for (const key in classes) {
- if (classes[key]) {
- classlist.push(key);
- }
- }
-
- attrs.class = classlist.join(' ');
+ if (css_hash || classes) {
+ attrs.class = to_class(attrs.class, css_hash, classes);
}
let attr_str = '';
@@ -274,35 +238,23 @@ function style_object_to_string(style_object) {
.join(' ');
}
-/** @param {Record} style_object */
-export function add_styles(style_object) {
- const styles = style_object_to_string(style_object);
- return styles ? ` style="${styles}"` : '';
+/**
+ * @param {any} value
+ * @param {string | undefined} [hash]
+ * @param {Record} [directives]
+ */
+export function attr_class(value, hash, directives) {
+ var result = to_class(value, hash, directives);
+ return result ? ` class="${escape_html(result, true)}"` : '';
}
/**
- * @param {string} attribute
- * @param {Record} styles
+ * @param {any} value
+ * @param {Record|[Record,Record]} [directives]
*/
-export function merge_styles(attribute, styles) {
- /** @type {Record} */
- var merged = {};
-
- if (attribute) {
- for (var declaration of attribute.split(';')) {
- var i = declaration.indexOf(':');
- var name = declaration.slice(0, i).trim();
- var value = declaration.slice(i + 1).trim();
-
- if (name !== '') merged[name] = value;
- }
- }
-
- for (name in styles) {
- merged[name] = styles[name];
- }
-
- return merged;
+export function attr_style(value, directives) {
+ var result = to_style(value, directives);
+ return result ? ` style="${escape_html(result, true)}"` : '';
}
/**
@@ -475,18 +427,21 @@ export function bind_props(props_parent, props_now) {
/**
* @template V
+ * @param {Payload} payload
* @param {Promise} promise
* @param {null | (() => void)} pending_fn
* @param {(value: V) => void} then_fn
* @returns {void}
*/
-function await_block(promise, pending_fn, then_fn) {
+function await_block(payload, promise, pending_fn, then_fn) {
if (is_promise(promise)) {
+ payload.out += BLOCK_OPEN;
promise.then(null, noop);
if (pending_fn !== null) {
pending_fn();
}
} else if (then_fn !== null) {
+ payload.out += BLOCK_OPEN_ELSE;
then_fn(promise);
}
}
@@ -526,13 +481,26 @@ 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';
export { push, pop } from './context.js';
-export { push_element, pop_element } from './dev.js';
+export { push_element, pop_element, validate_snippet_args } from './dev.js';
+
+export { assign_payload, copy_payload } from './payload.js';
export { snapshot } from '../shared/clone.js';
@@ -541,7 +509,8 @@ export { fallback } from '../shared/utils.js';
export {
invalid_default_snippet,
validate_dynamic_element_tag,
- validate_void_dynamic_element
+ validate_void_dynamic_element,
+ prevent_snippet_stringification
} from '../shared/validate.js';
export { escape_html as escape };
diff --git a/packages/svelte/src/internal/server/payload.js b/packages/svelte/src/internal/server/payload.js
new file mode 100644
index 0000000000..8df5787ba4
--- /dev/null
+++ b/packages/svelte/src/internal/server/payload.js
@@ -0,0 +1,72 @@
+export class HeadPayload {
+ /** @type {Set<{ hash: string; code: string }>} */
+ css = new Set();
+ out = '';
+ uid = () => '';
+ title = '';
+
+ constructor(css = new Set(), out = '', title = '', uid = () => '') {
+ this.css = css;
+ this.out = out;
+ this.title = title;
+ this.uid = uid;
+ }
+}
+
+export class Payload {
+ /** @type {Set<{ hash: string; code: string }>} */
+ css = new Set();
+ out = '';
+ uid = () => '';
+
+ head = new HeadPayload();
+
+ constructor(id_prefix = '') {
+ this.uid = props_id_generator(id_prefix);
+ this.head.uid = this.uid;
+ }
+}
+
+/**
+ * Used in legacy mode to handle bindings
+ * @param {Payload} to_copy
+ * @returns {Payload}
+ */
+export function copy_payload({ out, css, head, uid }) {
+ const payload = new Payload();
+
+ payload.out = out;
+ payload.css = new Set(css);
+ payload.uid = uid;
+
+ payload.head = new HeadPayload();
+ payload.head.out = head.out;
+ payload.head.css = new Set(head.css);
+ payload.head.title = head.title;
+ payload.head.uid = head.uid;
+
+ return payload;
+}
+
+/**
+ * Assigns second payload to first
+ * @param {Payload} p1
+ * @param {Payload} p2
+ * @returns {void}
+ */
+export function assign_payload(p1, p2) {
+ p1.out = p2.out;
+ p1.css = p2.css;
+ p1.head = p2.head;
+ p1.uid = p2.uid;
+}
+
+/**
+ * Creates an ID generator
+ * @param {string} prefix
+ * @returns {() => string}
+ */
+function props_id_generator(prefix) {
+ let uid = 1;
+ return () => `${prefix}s${uid++}`;
+}
diff --git a/packages/svelte/src/internal/server/types.d.ts b/packages/svelte/src/internal/server/types.d.ts
index e6c235147b..6b0fc146c4 100644
--- a/packages/svelte/src/internal/server/types.d.ts
+++ b/packages/svelte/src/internal/server/types.d.ts
@@ -11,15 +11,6 @@ export interface Component {
function?: any;
}
-export interface Payload {
- out: string;
- css: Set<{ hash: string; code: string }>;
- head: {
- title: string;
- out: string;
- };
-}
-
export interface RenderOutput {
/** HTML that goes into the `` */
head: string;
diff --git a/packages/svelte/src/internal/shared/attributes.js b/packages/svelte/src/internal/shared/attributes.js
index a561501bf4..c8758c1d4d 100644
--- a/packages/svelte/src/internal/shared/attributes.js
+++ b/packages/svelte/src/internal/shared/attributes.js
@@ -22,7 +22,7 @@ const replacements = {
* @returns {string}
*/
export function attr(name, value, is_boolean = false) {
- if (value == null || (!value && is_boolean) || (value === '' && name === 'class')) return '';
+ if (value == null || (!value && is_boolean)) return '';
const normalized = (name in replacements && replacements[name].get(value)) || value;
const assignment = is_boolean ? '' : `="${escape_html(normalized, true)}"`;
return ` ${name}${assignment}`;
@@ -40,3 +40,180 @@ export function clsx(value) {
return value ?? '';
}
}
+
+const whitespace = [...' \t\n\r\f\u00a0\u000b\ufeff'];
+
+/**
+ * @param {any} value
+ * @param {string | null} [hash]
+ * @param {Record} [directives]
+ * @returns {string | null}
+ */
+export function to_class(value, hash, directives) {
+ var classname = value == null ? '' : '' + value;
+
+ if (hash) {
+ classname = classname ? classname + ' ' + hash : hash;
+ }
+
+ if (directives) {
+ for (var key in directives) {
+ if (directives[key]) {
+ classname = classname ? classname + ' ' + key : key;
+ } else if (classname.length) {
+ var len = key.length;
+ var a = 0;
+
+ while ((a = classname.indexOf(key, a)) >= 0) {
+ var b = a + len;
+
+ if (
+ (a === 0 || whitespace.includes(classname[a - 1])) &&
+ (b === classname.length || whitespace.includes(classname[b]))
+ ) {
+ classname = (a === 0 ? '' : classname.substring(0, a)) + classname.substring(b + 1);
+ } else {
+ a = b;
+ }
+ }
+ }
+ }
+ }
+
+ return classname === '' ? null : classname;
+}
+
+/**
+ *
+ * @param {Record} styles
+ * @param {boolean} important
+ */
+function append_styles(styles, important = false) {
+ var separator = important ? ' !important;' : ';';
+ var css = '';
+
+ for (var key in styles) {
+ var value = styles[key];
+ if (value != null && value !== '') {
+ css += ' ' + key + ': ' + value + separator;
+ }
+ }
+
+ return css;
+}
+
+/**
+ * @param {string} name
+ * @returns {string}
+ */
+function to_css_name(name) {
+ if (name[0] !== '-' || name[1] !== '-') {
+ return name.toLowerCase();
+ }
+ return name;
+}
+
+/**
+ * @param {any} value
+ * @param {Record | [Record, Record]} [styles]
+ * @returns {string | null}
+ */
+export function to_style(value, styles) {
+ if (styles) {
+ var new_style = '';
+
+ /** @type {Record | undefined} */
+ var normal_styles;
+
+ /** @type {Record | undefined} */
+ var important_styles;
+
+ if (Array.isArray(styles)) {
+ normal_styles = styles[0];
+ important_styles = styles[1];
+ } else {
+ normal_styles = styles;
+ }
+
+ if (value) {
+ value = String(value)
+ .replaceAll(/\s*\/\*.*?\*\/\s*/g, '')
+ .trim();
+
+ /** @type {boolean | '"' | "'"} */
+ var in_str = false;
+ var in_apo = 0;
+ var in_comment = false;
+
+ var reserved_names = [];
+
+ if (normal_styles) {
+ reserved_names.push(...Object.keys(normal_styles).map(to_css_name));
+ }
+ if (important_styles) {
+ reserved_names.push(...Object.keys(important_styles).map(to_css_name));
+ }
+
+ var start_index = 0;
+ var name_index = -1;
+
+ const len = value.length;
+ for (var i = 0; i < len; i++) {
+ var c = value[i];
+
+ if (in_comment) {
+ if (c === '/' && value[i - 1] === '*') {
+ in_comment = false;
+ }
+ } else if (in_str) {
+ if (in_str === c) {
+ in_str = false;
+ }
+ } else if (c === '/' && value[i + 1] === '*') {
+ in_comment = true;
+ } else if (c === '"' || c === "'") {
+ in_str = c;
+ } else if (c === '(') {
+ in_apo++;
+ } else if (c === ')') {
+ in_apo--;
+ }
+
+ if (!in_comment && in_str === false && in_apo === 0) {
+ if (c === ':' && name_index === -1) {
+ name_index = i;
+ } else if (c === ';' || i === len - 1) {
+ if (name_index !== -1) {
+ var name = to_css_name(value.substring(start_index, name_index).trim());
+
+ if (!reserved_names.includes(name)) {
+ if (c !== ';') {
+ i++;
+ }
+
+ var property = value.substring(start_index, i).trim();
+ new_style += ' ' + property + ';';
+ }
+ }
+
+ start_index = i + 1;
+ name_index = -1;
+ }
+ }
+ }
+ }
+
+ if (normal_styles) {
+ new_style += append_styles(normal_styles);
+ }
+
+ if (important_styles) {
+ new_style += append_styles(important_styles, true);
+ }
+
+ new_style = new_style.trim();
+ return new_style === '' ? null : new_style;
+ }
+
+ return value == null ? null : String(value);
+}
diff --git a/packages/svelte/src/internal/shared/errors.js b/packages/svelte/src/internal/shared/errors.js
index 26d6822cdb..b8606fbf6f 100644
--- a/packages/svelte/src/internal/shared/errors.js
+++ b/packages/svelte/src/internal/shared/errors.js
@@ -17,6 +17,21 @@ export function invalid_default_snippet() {
}
}
+/**
+ * A snippet function was passed invalid arguments. Snippets should only be instantiated via `{@render ...}`
+ * @returns {never}
+ */
+export function invalid_snippet_arguments() {
+ if (DEV) {
+ const error = new Error(`invalid_snippet_arguments\nA snippet function was passed invalid arguments. Snippets should only be instantiated via \`{@render ...}\`\nhttps://svelte.dev/e/invalid_snippet_arguments`);
+
+ error.name = 'Svelte error';
+ throw error;
+ } else {
+ throw new Error(`https://svelte.dev/e/invalid_snippet_arguments`);
+ }
+}
+
/**
* `%name%(...)` can only be used during component initialisation
* @param {string} name
@@ -33,6 +48,21 @@ export function lifecycle_outside_component(name) {
}
}
+/**
+ * Attempted to render a snippet without a `{@render}` block. This would cause the snippet code to be stringified instead of its content being rendered to the DOM. To fix this, change `{snippet}` to `{@render snippet()}`.
+ * @returns {never}
+ */
+export function snippet_without_render_tag() {
+ if (DEV) {
+ const error = new Error(`snippet_without_render_tag\nAttempted to render a snippet without a \`{@render}\` block. This would cause the snippet code to be stringified instead of its content being rendered to the DOM. To fix this, change \`{snippet}\` to \`{@render snippet()}\`.\nhttps://svelte.dev/e/snippet_without_render_tag`);
+
+ error.name = 'Svelte error';
+ throw error;
+ } else {
+ throw new Error(`https://svelte.dev/e/snippet_without_render_tag`);
+ }
+}
+
/**
* `%name%` is not a store with a `subscribe` method
* @param {string} name
diff --git a/packages/svelte/src/internal/shared/utils.js b/packages/svelte/src/internal/shared/utils.js
index f9d52cb065..5e7f3152d8 100644
--- a/packages/svelte/src/internal/shared/utils.js
+++ b/packages/svelte/src/internal/shared/utils.js
@@ -10,6 +10,7 @@ export var get_descriptors = Object.getOwnPropertyDescriptors;
export var object_prototype = Object.prototype;
export var array_prototype = Array.prototype;
export var get_prototype_of = Object.getPrototypeOf;
+export var is_extensible = Object.isExtensible;
/**
* @param {any} thing
diff --git a/packages/svelte/src/internal/shared/validate.js b/packages/svelte/src/internal/shared/validate.js
index 852c0e83bf..bbb237594b 100644
--- a/packages/svelte/src/internal/shared/validate.js
+++ b/packages/svelte/src/internal/shared/validate.js
@@ -35,3 +35,15 @@ export function validate_store(store, name) {
e.store_invalid_shape(name);
}
}
+
+/**
+ * @template {() => unknown} T
+ * @param {T} fn
+ */
+export function prevent_snippet_stringification(fn) {
+ fn.toString = () => {
+ e.snippet_without_render_tag();
+ return '';
+ };
+ return fn;
+}
diff --git a/packages/svelte/src/legacy/legacy-client.js b/packages/svelte/src/legacy/legacy-client.js
index 3715617f4c..bb9a5a9c03 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, flushSync, 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.
@@ -125,9 +119,9 @@ class Svelte4Component {
recover: options.recover
});
- // We don't flush_sync for custom element wrappers or if the user doesn't want it
+ // We don't flushSync for custom element wrappers or if the user doesn't want it
if (!options?.props?.$$host || options.sync === false) {
- flush_sync();
+ flushSync();
}
this.#events = props.$$events;
diff --git a/packages/svelte/src/reactivity/date.js b/packages/svelte/src/reactivity/date.js
index 33da2e1761..721673bc36 100644
--- a/packages/svelte/src/reactivity/date.js
+++ b/packages/svelte/src/reactivity/date.js
@@ -5,6 +5,38 @@ import { active_reaction, get, set_active_reaction } from '../internal/client/ru
var inited = false;
+/**
+ * A reactive version of the built-in [`Date`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date) object.
+ * Reading the date (whether with methods like `date.getTime()` or `date.toString()`, or via things like [`Intl.DateTimeFormat`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat))
+ * in an [effect](https://svelte.dev/docs/svelte/$effect) or [derived](https://svelte.dev/docs/svelte/$derived)
+ * will cause it to be re-evaluated when the value of the date changes.
+ *
+ * ```svelte
+ *
+ *
+ * The time is {formatter.format(date)}
+ * ```
+ */
export class SvelteDate extends Date {
#time = source(super.getTime());
diff --git a/packages/svelte/src/reactivity/map.js b/packages/svelte/src/reactivity/map.js
index 3fa2945ef0..3ae8fe5ad1 100644
--- a/packages/svelte/src/reactivity/map.js
+++ b/packages/svelte/src/reactivity/map.js
@@ -5,6 +5,47 @@ import { get } from '../internal/client/runtime.js';
import { increment } from './utils.js';
/**
+ * A reactive version of the built-in [`Map`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map) object.
+ * Reading contents of the map (by iterating, or by reading `map.size` or calling `map.get(...)` or `map.has(...)` as in the [tic-tac-toe example](https://svelte.dev/playground/0b0ff4aa49c9443f9b47fe5203c78293) below) in an [effect](https://svelte.dev/docs/svelte/$effect) or [derived](https://svelte.dev/docs/svelte/$derived)
+ * will cause it to be re-evaluated as necessary when the map is updated.
+ *
+ * Note that values in a reactive map are _not_ made [deeply reactive](https://svelte.dev/docs/svelte/$state#Deep-state).
+ *
+ * ```svelte
+ *
+ *
+ *
+ * {#each Array(9), i}
+ * {
+ * board.set(i, player);
+ * player = player === 'x' ? 'o' : 'x';
+ * }}
+ * >{board.get(i)}
+ * {/each}
+ *
+ *
+ * {#if winner}
+ * {winner} wins!
+ * reset
+ * {:else}
+ * {player} is next
+ * {/if}
+ * ```
+ *
* @template K
* @template V
* @extends {Map}
diff --git a/packages/svelte/src/reactivity/set.js b/packages/svelte/src/reactivity/set.js
index be0c2d2cf5..4a0b4dfdb3 100644
--- a/packages/svelte/src/reactivity/set.js
+++ b/packages/svelte/src/reactivity/set.js
@@ -10,6 +10,37 @@ var set_like_methods = ['difference', 'intersection', 'symmetricDifference', 'un
var inited = false;
/**
+ * A reactive version of the built-in [`Set`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set) object.
+ * Reading contents of the set (by iterating, or by reading `set.size` or calling `set.has(...)` as in the [example](https://svelte.dev/playground/53438b51194b4882bcc18cddf9f96f15) below) in an [effect](https://svelte.dev/docs/svelte/$effect) or [derived](https://svelte.dev/docs/svelte/$derived)
+ * will cause it to be re-evaluated as necessary when the set is updated.
+ *
+ * Note that values in a reactive set are _not_ made [deeply reactive](https://svelte.dev/docs/svelte/$state#Deep-state).
+ *
+ * ```svelte
+ *
+ *
+ * {#each ['🙈', '🙉', '🙊'] as monkey}
+ * toggle(monkey)}>{monkey}
+ * {/each}
+ *
+ * monkeys.clear()}>clear
+ *
+ * {#if monkeys.has('🙈')}see no evil
{/if}
+ * {#if monkeys.has('🙉')}hear no evil
{/if}
+ * {#if monkeys.has('🙊')}speak no evil
{/if}
+ * ```
+ *
* @template T
* @extends {Set}
*/
diff --git a/packages/svelte/src/reactivity/url-search-params.js b/packages/svelte/src/reactivity/url-search-params.js
index 13f6971996..c1a8275f15 100644
--- a/packages/svelte/src/reactivity/url-search-params.js
+++ b/packages/svelte/src/reactivity/url-search-params.js
@@ -5,6 +5,32 @@ import { increment } from './utils.js';
export const REPLACE = Symbol();
+/**
+ * A reactive version of the built-in [`URLSearchParams`](https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams) object.
+ * Reading its contents (by iterating, or by calling `params.get(...)` or `params.getAll(...)` as in the [example](https://svelte.dev/playground/b3926c86c5384bab9f2cf993bc08c1c8) below) in an [effect](https://svelte.dev/docs/svelte/$effect) or [derived](https://svelte.dev/docs/svelte/$derived)
+ * will cause it to be re-evaluated as necessary when the params are updated.
+ *
+ * ```svelte
+ *
+ *
+ *
+ *
+ * params.append(key, value)}>append
+ *
+ * ?{params.toString()}
+ *
+ * {#each params as [key, value]}
+ * {key}: {value}
+ * {/each}
+ * ```
+ */
export class SvelteURLSearchParams extends URLSearchParams {
#version = source(0);
#url = get_current_url();
@@ -23,6 +49,7 @@ export class SvelteURLSearchParams extends URLSearchParams {
/**
* @param {URLSearchParams} params
+ * @internal
*/
[REPLACE](params) {
if (this.#updating) return;
diff --git a/packages/svelte/src/reactivity/url.js b/packages/svelte/src/reactivity/url.js
index 5d003be021..879006f057 100644
--- a/packages/svelte/src/reactivity/url.js
+++ b/packages/svelte/src/reactivity/url.js
@@ -10,6 +10,33 @@ export function get_current_url() {
return current_url;
}
+/**
+ * A reactive version of the built-in [`URL`](https://developer.mozilla.org/en-US/docs/Web/API/URL) object.
+ * Reading properties of the URL (such as `url.href` or `url.pathname`) in an [effect](https://svelte.dev/docs/svelte/$effect) or [derived](https://svelte.dev/docs/svelte/$derived)
+ * will cause it to be re-evaluated as necessary when the URL changes.
+ *
+ * The `searchParams` property is an instance of [SvelteURLSearchParams](https://svelte.dev/docs/svelte/svelte-reactivity#SvelteURLSearchParams).
+ *
+ * [Example](https://svelte.dev/playground/5a694758901b448c83dc40dc31c71f2a):
+ *
+ * ```svelte
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ * ```
+ */
export class SvelteURL extends URL {
#protocol = source(super.protocol);
#username = source(super.username);
diff --git a/packages/svelte/src/server/index.d.ts b/packages/svelte/src/server/index.d.ts
index b65ce5bdaa..d5a3b813e6 100644
--- a/packages/svelte/src/server/index.d.ts
+++ b/packages/svelte/src/server/index.d.ts
@@ -12,10 +12,18 @@ export function render<
...args: {} extends Props
? [
component: Comp extends SvelteComponent ? ComponentType : Comp,
- options?: { props?: Omit; context?: Map }
+ options?: {
+ props?: Omit;
+ context?: Map;
+ idPrefix?: string;
+ }
]
: [
component: Comp extends SvelteComponent ? ComponentType : Comp,
- options: { props: Omit; context?: Map }
+ options: {
+ props: Omit;
+ context?: Map;
+ idPrefix?: string;
+ }
]
): RenderOutput;
diff --git a/packages/svelte/src/store/index-client.js b/packages/svelte/src/store/index-client.js
index ae6806ec76..2f0a1a831a 100644
--- a/packages/svelte/src/store/index-client.js
+++ b/packages/svelte/src/store/index-client.js
@@ -6,6 +6,12 @@ import {
} from '../internal/client/reactivity/effects.js';
import { get, writable } from './shared/index.js';
import { createSubscriber } from '../reactivity/create-subscriber.js';
+import {
+ active_effect,
+ active_reaction,
+ set_active_effect,
+ set_active_reaction
+} from '../internal/client/runtime.js';
export { derived, get, readable, readonly, writable } from './shared/index.js';
@@ -39,19 +45,34 @@ export { derived, get, readable, readonly, writable } from './shared/index.js';
* @returns {Writable | Readable}
*/
export function toStore(get, set) {
- let init_value = get();
+ var effect = active_effect;
+ var reaction = active_reaction;
+ var init_value = get();
+
const store = writable(init_value, (set) => {
// If the value has changed before we call subscribe, then
// we need to treat the value as already having run
- let ran = init_value !== get();
+ var ran = init_value !== get();
// TODO do we need a different implementation on the server?
- const teardown = effect_root(() => {
- render_effect(() => {
- const value = get();
- if (ran) set(value);
+ var teardown;
+ // Apply the reaction and effect at the time of toStore being called
+ var previous_reaction = active_reaction;
+ var previous_effect = active_effect;
+ set_active_reaction(reaction);
+ set_active_effect(effect);
+
+ try {
+ teardown = effect_root(() => {
+ render_effect(() => {
+ const value = get();
+ if (ran) set(value);
+ });
});
- });
+ } finally {
+ set_active_reaction(previous_reaction);
+ set_active_effect(previous_effect);
+ }
ran = true;
diff --git a/packages/svelte/src/utils.js b/packages/svelte/src/utils.js
index 76486d32ac..ada318e85a 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',
@@ -453,8 +465,10 @@ export function is_raw_text_element(name) {
/**
* Prevent devtools trying to make `location` a clickable link by inserting a zero-width space
- * @param {string | undefined} location
+ * @template {string | undefined} T
+ * @param {T} location
+ * @returns {T};
*/
export function sanitize_location(location) {
- return location?.replace(/\//g, '/\u200b');
+ return /** @type {T} */ (location?.replace(/\//g, '/\u200b'));
}
diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js
index 0fd7d2f039..62624e866c 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.17.3';
+export const VERSION = '5.27.2';
export const PUBLIC_VERSION = '5';
diff --git a/packages/svelte/tests/compiler-errors/samples/css-global-block-multiple-1/_config.js b/packages/svelte/tests/compiler-errors/samples/css-global-block-multiple-1/_config.js
new file mode 100644
index 0000000000..85dedc8012
--- /dev/null
+++ b/packages/svelte/tests/compiler-errors/samples/css-global-block-multiple-1/_config.js
@@ -0,0 +1,10 @@
+import { test } from '../../test';
+
+export default test({
+ error: {
+ code: 'css_global_block_invalid_list',
+ message:
+ "A `:global` selector cannot be part of a selector list with entries that don't contain `:global`",
+ position: [232, 246]
+ }
+});
diff --git a/packages/svelte/tests/compiler-errors/samples/css-global-block-multiple-1/main.svelte b/packages/svelte/tests/compiler-errors/samples/css-global-block-multiple-1/main.svelte
new file mode 100644
index 0000000000..260921f704
--- /dev/null
+++ b/packages/svelte/tests/compiler-errors/samples/css-global-block-multiple-1/main.svelte
@@ -0,0 +1,9 @@
+
diff --git a/packages/svelte/tests/compiler-errors/samples/css-global-block-multiple-2/_config.js b/packages/svelte/tests/compiler-errors/samples/css-global-block-multiple-2/_config.js
new file mode 100644
index 0000000000..f24095800a
--- /dev/null
+++ b/packages/svelte/tests/compiler-errors/samples/css-global-block-multiple-2/_config.js
@@ -0,0 +1,10 @@
+import { test } from '../../test';
+
+export default test({
+ error: {
+ code: 'css_global_block_invalid_list',
+ message:
+ "A `:global` selector cannot be part of a selector list with entries that don't contain `:global`",
+ position: [24, 43]
+ }
+});
diff --git a/packages/svelte/tests/compiler-errors/samples/css-global-block-multiple-2/main.svelte b/packages/svelte/tests/compiler-errors/samples/css-global-block-multiple-2/main.svelte
new file mode 100644
index 0000000000..2a09ec10ce
--- /dev/null
+++ b/packages/svelte/tests/compiler-errors/samples/css-global-block-multiple-2/main.svelte
@@ -0,0 +1,6 @@
+
diff --git a/packages/svelte/tests/compiler-errors/samples/css-global-block-multiple/_config.js b/packages/svelte/tests/compiler-errors/samples/css-global-block-multiple/_config.js
deleted file mode 100644
index 9ae4e758c4..0000000000
--- a/packages/svelte/tests/compiler-errors/samples/css-global-block-multiple/_config.js
+++ /dev/null
@@ -1,9 +0,0 @@
-import { test } from '../../test';
-
-export default test({
- error: {
- code: 'css_global_block_invalid_list',
- message: 'A `:global` selector cannot be part of a selector list with more than one item',
- position: [9, 31]
- }
-});
diff --git a/packages/svelte/tests/compiler-errors/samples/css-global-block-multiple/main.svelte b/packages/svelte/tests/compiler-errors/samples/css-global-block-multiple/main.svelte
deleted file mode 100644
index 75178bc664..0000000000
--- a/packages/svelte/tests/compiler-errors/samples/css-global-block-multiple/main.svelte
+++ /dev/null
@@ -1,3 +0,0 @@
-
diff --git a/packages/svelte/tests/compiler-errors/samples/runes-no-derived-assignment/_config.js b/packages/svelte/tests/compiler-errors/samples/runes-no-derived-assignment/_config.js
deleted file mode 100644
index 94985a9939..0000000000
--- a/packages/svelte/tests/compiler-errors/samples/runes-no-derived-assignment/_config.js
+++ /dev/null
@@ -1,8 +0,0 @@
-import { test } from '../../test';
-
-export default test({
- error: {
- code: 'constant_assignment',
- message: 'Cannot assign to derived state'
- }
-});
diff --git a/packages/svelte/tests/compiler-errors/samples/runes-no-derived-assignment/main.svelte b/packages/svelte/tests/compiler-errors/samples/runes-no-derived-assignment/main.svelte
deleted file mode 100644
index 3bf836f6c5..0000000000
--- a/packages/svelte/tests/compiler-errors/samples/runes-no-derived-assignment/main.svelte
+++ /dev/null
@@ -1,5 +0,0 @@
-
diff --git a/packages/svelte/tests/compiler-errors/samples/runes-no-derived-binding/_config.js b/packages/svelte/tests/compiler-errors/samples/runes-no-derived-binding/_config.js
deleted file mode 100644
index 87b88d79cc..0000000000
--- a/packages/svelte/tests/compiler-errors/samples/runes-no-derived-binding/_config.js
+++ /dev/null
@@ -1,8 +0,0 @@
-import { test } from '../../test';
-
-export default test({
- error: {
- code: 'constant_binding',
- message: 'Cannot bind to derived state'
- }
-});
diff --git a/packages/svelte/tests/compiler-errors/samples/runes-no-derived-binding/main.svelte b/packages/svelte/tests/compiler-errors/samples/runes-no-derived-binding/main.svelte
deleted file mode 100644
index 6c198dc068..0000000000
--- a/packages/svelte/tests/compiler-errors/samples/runes-no-derived-binding/main.svelte
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
diff --git a/packages/svelte/tests/compiler-errors/samples/runes-no-derived-state-field-assignment/_config.js b/packages/svelte/tests/compiler-errors/samples/runes-no-derived-state-field-assignment/_config.js
deleted file mode 100644
index 94985a9939..0000000000
--- a/packages/svelte/tests/compiler-errors/samples/runes-no-derived-state-field-assignment/_config.js
+++ /dev/null
@@ -1,8 +0,0 @@
-import { test } from '../../test';
-
-export default test({
- error: {
- code: 'constant_assignment',
- message: 'Cannot assign to derived state'
- }
-});
diff --git a/packages/svelte/tests/compiler-errors/samples/runes-no-derived-state-field-assignment/main.svelte b/packages/svelte/tests/compiler-errors/samples/runes-no-derived-state-field-assignment/main.svelte
deleted file mode 100644
index d44806757e..0000000000
--- a/packages/svelte/tests/compiler-errors/samples/runes-no-derived-state-field-assignment/main.svelte
+++ /dev/null
@@ -1,10 +0,0 @@
-
diff --git a/packages/svelte/tests/compiler-errors/samples/runes-no-derived-state-field-update/_config.js b/packages/svelte/tests/compiler-errors/samples/runes-no-derived-state-field-update/_config.js
deleted file mode 100644
index 94985a9939..0000000000
--- a/packages/svelte/tests/compiler-errors/samples/runes-no-derived-state-field-update/_config.js
+++ /dev/null
@@ -1,8 +0,0 @@
-import { test } from '../../test';
-
-export default test({
- error: {
- code: 'constant_assignment',
- message: 'Cannot assign to derived state'
- }
-});
diff --git a/packages/svelte/tests/compiler-errors/samples/runes-no-derived-state-field-update/main.svelte b/packages/svelte/tests/compiler-errors/samples/runes-no-derived-state-field-update/main.svelte
deleted file mode 100644
index e4ee2e8635..0000000000
--- a/packages/svelte/tests/compiler-errors/samples/runes-no-derived-state-field-update/main.svelte
+++ /dev/null
@@ -1,10 +0,0 @@
-
diff --git a/packages/svelte/tests/compiler-errors/samples/runes-no-derived-update/_config.js b/packages/svelte/tests/compiler-errors/samples/runes-no-derived-update/_config.js
deleted file mode 100644
index 94985a9939..0000000000
--- a/packages/svelte/tests/compiler-errors/samples/runes-no-derived-update/_config.js
+++ /dev/null
@@ -1,8 +0,0 @@
-import { test } from '../../test';
-
-export default test({
- error: {
- code: 'constant_assignment',
- message: 'Cannot assign to derived state'
- }
-});
diff --git a/packages/svelte/tests/compiler-errors/samples/runes-no-derived-update/main.svelte b/packages/svelte/tests/compiler-errors/samples/runes-no-derived-update/main.svelte
deleted file mode 100644
index d266c95bb8..0000000000
--- a/packages/svelte/tests/compiler-errors/samples/runes-no-derived-update/main.svelte
+++ /dev/null
@@ -1,5 +0,0 @@
-
diff --git a/packages/svelte/tests/compiler-errors/samples/runes-wrong-state-raw-args/_config.js b/packages/svelte/tests/compiler-errors/samples/runes-wrong-state-raw-args/_config.js
new file mode 100644
index 0000000000..af226559d1
--- /dev/null
+++ b/packages/svelte/tests/compiler-errors/samples/runes-wrong-state-raw-args/_config.js
@@ -0,0 +1,8 @@
+import { test } from '../../test';
+
+export default test({
+ error: {
+ code: 'rune_invalid_arguments_length',
+ message: '`$state.raw` must be called with zero or one arguments'
+ }
+});
diff --git a/packages/svelte/tests/compiler-errors/samples/runes-wrong-state-raw-args/main.svelte b/packages/svelte/tests/compiler-errors/samples/runes-wrong-state-raw-args/main.svelte
new file mode 100644
index 0000000000..2b50b43b9a
--- /dev/null
+++ b/packages/svelte/tests/compiler-errors/samples/runes-wrong-state-raw-args/main.svelte
@@ -0,0 +1,3 @@
+
diff --git a/packages/svelte/tests/compiler-errors/samples/runes-wrong-state-raw-args/main.svelte.js b/packages/svelte/tests/compiler-errors/samples/runes-wrong-state-raw-args/main.svelte.js
new file mode 100644
index 0000000000..442aaad142
--- /dev/null
+++ b/packages/svelte/tests/compiler-errors/samples/runes-wrong-state-raw-args/main.svelte.js
@@ -0,0 +1 @@
+const foo = $state.raw(1, 2, 3);
diff --git a/packages/svelte/tests/css/samples/double-hyphen/expected.css b/packages/svelte/tests/css/samples/double-hyphen/expected.css
new file mode 100644
index 0000000000..ab4921b657
--- /dev/null
+++ b/packages/svelte/tests/css/samples/double-hyphen/expected.css
@@ -0,0 +1,4 @@
+
+ .--foo.svelte-xyz {
+ color: red;
+ }
diff --git a/packages/svelte/tests/css/samples/double-hyphen/input.svelte b/packages/svelte/tests/css/samples/double-hyphen/input.svelte
new file mode 100644
index 0000000000..e2907f9344
--- /dev/null
+++ b/packages/svelte/tests/css/samples/double-hyphen/input.svelte
@@ -0,0 +1,7 @@
+
+
+
\ No newline at end of file
diff --git a/packages/svelte/tests/css/samples/general-siblings-combinator-slot/_config.js b/packages/svelte/tests/css/samples/general-siblings-combinator-slot/_config.js
index 97e470d1c3..7644865495 100644
--- a/packages/svelte/tests/css/samples/general-siblings-combinator-slot/_config.js
+++ b/packages/svelte/tests/css/samples/general-siblings-combinator-slot/_config.js
@@ -5,32 +5,20 @@ export default test({
{
code: 'css_unused_selector',
message: 'Unused CSS selector ".b ~ .c"',
- start: { character: 137, column: 1, line: 11 },
- end: { character: 144, column: 8, line: 11 }
+ start: { character: 191, column: 1, line: 13 },
+ end: { character: 198, column: 8, line: 13 }
},
{
code: 'css_unused_selector',
message: 'Unused CSS selector ".c ~ .f"',
- start: { character: 162, column: 1, line: 12 },
- end: { character: 169, column: 8, line: 12 }
- },
- {
- code: 'css_unused_selector',
- message: 'Unused CSS selector ".f ~ .g"',
- start: { character: 187, column: 1, line: 13 },
- end: { character: 194, column: 8, line: 13 }
+ start: { character: 216, column: 1, line: 14 },
+ end: { character: 223, column: 8, line: 14 }
},
{
code: 'css_unused_selector',
message: 'Unused CSS selector ".b ~ .f"',
- start: { character: 212, column: 1, line: 14 },
- end: { character: 219, column: 8, line: 14 }
- },
- {
- code: 'css_unused_selector',
- message: 'Unused CSS selector ".b ~ .g"',
- start: { character: 237, column: 1, line: 15 },
- end: { character: 244, column: 8, line: 15 }
+ start: { character: 241, column: 1, line: 15 },
+ end: { character: 248, column: 8, line: 15 }
}
]
});
diff --git a/packages/svelte/tests/css/samples/general-siblings-combinator-slot/expected.css b/packages/svelte/tests/css/samples/general-siblings-combinator-slot/expected.css
index 67a19d10c9..53fca3ae9e 100644
--- a/packages/svelte/tests/css/samples/general-siblings-combinator-slot/expected.css
+++ b/packages/svelte/tests/css/samples/general-siblings-combinator-slot/expected.css
@@ -2,10 +2,10 @@
.d.svelte-xyz ~ .e:where(.svelte-xyz) { color: green; }
.a.svelte-xyz ~ .g:where(.svelte-xyz) { color: green; }
.a.svelte-xyz ~ .b:where(.svelte-xyz) { color: green; }
+ .f.svelte-xyz ~ .g:where(.svelte-xyz) { color: green; }
+ .b.svelte-xyz ~ .g:where(.svelte-xyz) { color: green; }
/* no match */
/* (unused) .b ~ .c { color: red; }*/
/* (unused) .c ~ .f { color: red; }*/
- /* (unused) .f ~ .g { color: red; }*/
/* (unused) .b ~ .f { color: red; }*/
- /* (unused) .b ~ .g { color: red; }*/
diff --git a/packages/svelte/tests/css/samples/general-siblings-combinator-slot/input.svelte b/packages/svelte/tests/css/samples/general-siblings-combinator-slot/input.svelte
index 2e2846fa87..52264d3a5a 100644
--- a/packages/svelte/tests/css/samples/general-siblings-combinator-slot/input.svelte
+++ b/packages/svelte/tests/css/samples/general-siblings-combinator-slot/input.svelte
@@ -6,13 +6,13 @@
.d ~ .e { color: green; }
.a ~ .g { color: green; }
.a ~ .b { color: green; }
+ .f ~ .g { color: green; }
+ .b ~ .g { color: green; }
/* no match */
.b ~ .c { color: red; }
.c ~ .f { color: red; }
- .f ~ .g { color: red; }
.b ~ .f { color: red; }
- .b ~ .g { color: red; }
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/global-block/_config.js b/packages/svelte/tests/css/samples/global-block/_config.js
index bee0d7204d..a8b11a73ec 100644
--- a/packages/svelte/tests/css/samples/global-block/_config.js
+++ b/packages/svelte/tests/css/samples/global-block/_config.js
@@ -16,6 +16,20 @@ export default test({
column: 16,
character: 932
}
+ },
+ {
+ code: 'css_unused_selector',
+ message: 'Unused CSS selector "unused :global"',
+ start: {
+ line: 100,
+ column: 29,
+ character: 1223
+ },
+ end: {
+ line: 100,
+ column: 43,
+ character: 1237
+ }
}
]
});
diff --git a/packages/svelte/tests/css/samples/global-block/expected.css b/packages/svelte/tests/css/samples/global-block/expected.css
index 438749224b..12f9a75032 100644
--- a/packages/svelte/tests/css/samples/global-block/expected.css
+++ b/packages/svelte/tests/css/samples/global-block/expected.css
@@ -90,3 +90,13 @@
opacity: 1;
}
}
+
+ x, y {
+ color: green;
+ }
+
+ div.svelte-xyz, div.svelte-xyz y /* (unused) unused*/ {
+ z {
+ color: green;
+ }
+ }
diff --git a/packages/svelte/tests/css/samples/global-block/input.svelte b/packages/svelte/tests/css/samples/global-block/input.svelte
index a1833636a1..ee05205d67 100644
--- a/packages/svelte/tests/css/samples/global-block/input.svelte
+++ b/packages/svelte/tests/css/samples/global-block/input.svelte
@@ -92,4 +92,14 @@
opacity: 1;
}
}
+
+ :global x, :global y {
+ color: green;
+ }
+
+ div :global, div :global y, unused :global {
+ z {
+ color: green;
+ }
+ }
diff --git a/packages/svelte/tests/css/samples/global-keyframes/_config.js b/packages/svelte/tests/css/samples/global-keyframes/_config.js
new file mode 100644
index 0000000000..30953854ad
--- /dev/null
+++ b/packages/svelte/tests/css/samples/global-keyframes/_config.js
@@ -0,0 +1,5 @@
+import { test } from '../../test';
+
+export default test({
+ hasGlobal: true
+});
diff --git a/packages/svelte/tests/css/samples/global-local-nested/_config.js b/packages/svelte/tests/css/samples/global-local-nested/_config.js
new file mode 100644
index 0000000000..5a7796ebac
--- /dev/null
+++ b/packages/svelte/tests/css/samples/global-local-nested/_config.js
@@ -0,0 +1,5 @@
+import { test } from '../../test';
+
+export default test({
+ hasGlobal: false
+});
diff --git a/packages/svelte/tests/css/samples/global-local-nested/expected.css b/packages/svelte/tests/css/samples/global-local-nested/expected.css
new file mode 100644
index 0000000000..8eadf2b948
--- /dev/null
+++ b/packages/svelte/tests/css/samples/global-local-nested/expected.css
@@ -0,0 +1,12 @@
+
+ div.svelte-xyz {
+ .whatever {
+ color: green;
+ }
+ }
+
+ .whatever {
+ div.svelte-xyz {
+ color: green;
+ }
+ }
diff --git a/packages/svelte/tests/css/samples/global-local-nested/input.svelte b/packages/svelte/tests/css/samples/global-local-nested/input.svelte
new file mode 100644
index 0000000000..60210be753
--- /dev/null
+++ b/packages/svelte/tests/css/samples/global-local-nested/input.svelte
@@ -0,0 +1,15 @@
+{@html whatever}
+
+
diff --git a/packages/svelte/tests/css/samples/global-local/_config.js b/packages/svelte/tests/css/samples/global-local/_config.js
new file mode 100644
index 0000000000..5a7796ebac
--- /dev/null
+++ b/packages/svelte/tests/css/samples/global-local/_config.js
@@ -0,0 +1,5 @@
+import { test } from '../../test';
+
+export default test({
+ hasGlobal: false
+});
diff --git a/packages/svelte/tests/css/samples/global-local/expected.css b/packages/svelte/tests/css/samples/global-local/expected.css
new file mode 100644
index 0000000000..c4fc74fb1a
--- /dev/null
+++ b/packages/svelte/tests/css/samples/global-local/expected.css
@@ -0,0 +1,8 @@
+
+ div.svelte-xyz .whatever {
+ color: green;
+ }
+
+ .whatever div.svelte-xyz {
+ color: green;
+ }
diff --git a/packages/svelte/tests/css/samples/global-local/input.svelte b/packages/svelte/tests/css/samples/global-local/input.svelte
new file mode 100644
index 0000000000..bff97ab485
--- /dev/null
+++ b/packages/svelte/tests/css/samples/global-local/input.svelte
@@ -0,0 +1,11 @@
+{@html whatever}
+
+
diff --git a/packages/svelte/tests/css/samples/global-with-nesting/_config.js b/packages/svelte/tests/css/samples/global-with-nesting/_config.js
index 292c6c49ac..6cec7c2360 100644
--- a/packages/svelte/tests/css/samples/global-with-nesting/_config.js
+++ b/packages/svelte/tests/css/samples/global-with-nesting/_config.js
@@ -1,5 +1,7 @@
import { test } from '../../test';
export default test({
- warnings: []
+ warnings: [],
+
+ hasGlobal: false
});
diff --git a/packages/svelte/tests/css/samples/global-with-nesting/expected.css b/packages/svelte/tests/css/samples/global-with-nesting/expected.css
index dcb8a0e481..1863c57d85 100644
--- a/packages/svelte/tests/css/samples/global-with-nesting/expected.css
+++ b/packages/svelte/tests/css/samples/global-with-nesting/expected.css
@@ -1,5 +1,10 @@
div.svelte-xyz {
&.class{
- color: red;
+ color: green;
+ }
+ }
+ * {
+ &:hover .class.svelte-xyz {
+ color: green;
}
}
\ No newline at end of file
diff --git a/packages/svelte/tests/css/samples/global-with-nesting/input.svelte b/packages/svelte/tests/css/samples/global-with-nesting/input.svelte
index 0c73ed7a78..2c1d2b5ebd 100644
--- a/packages/svelte/tests/css/samples/global-with-nesting/input.svelte
+++ b/packages/svelte/tests/css/samples/global-with-nesting/input.svelte
@@ -1,7 +1,12 @@
diff --git a/packages/svelte/tests/css/samples/global/_config.js b/packages/svelte/tests/css/samples/global/_config.js
new file mode 100644
index 0000000000..30953854ad
--- /dev/null
+++ b/packages/svelte/tests/css/samples/global/_config.js
@@ -0,0 +1,5 @@
+import { test } from '../../test';
+
+export default test({
+ hasGlobal: true
+});
diff --git a/packages/svelte/tests/css/samples/has/_config.js b/packages/svelte/tests/css/samples/has/_config.js
index e5dc5f3459..5700a09b96 100644
--- a/packages/svelte/tests/css/samples/has/_config.js
+++ b/packages/svelte/tests/css/samples/has/_config.js
@@ -6,182 +6,238 @@ export default test({
code: 'css_unused_selector',
message: 'Unused CSS selector ".unused:has(y)"',
start: {
- line: 31,
+ line: 41,
column: 1,
- character: 308
+ character: 378
},
end: {
- line: 31,
+ line: 41,
column: 15,
- character: 322
+ character: 392
}
},
{
code: 'css_unused_selector',
message: 'Unused CSS selector ".unused:has(:global(y))"',
start: {
- line: 34,
+ line: 44,
column: 1,
- character: 343
+ character: 413
},
end: {
- line: 34,
+ line: 44,
column: 24,
- character: 366
+ character: 436
}
},
{
code: 'css_unused_selector',
message: 'Unused CSS selector "x:has(.unused)"',
start: {
- line: 37,
+ line: 47,
column: 1,
- character: 387
+ character: 457
},
end: {
- line: 37,
+ line: 47,
column: 15,
- character: 401
+ character: 471
}
},
{
code: 'css_unused_selector',
message: 'Unused CSS selector ":global(.foo):has(.unused)"',
start: {
- line: 40,
+ line: 50,
column: 1,
- character: 422
+ character: 492
},
end: {
- line: 40,
+ line: 50,
column: 27,
- character: 448
+ character: 518
}
},
{
code: 'css_unused_selector',
message: 'Unused CSS selector "x:has(y):has(.unused)"',
start: {
- line: 50,
+ line: 60,
column: 1,
- character: 556
+ character: 626
},
end: {
- line: 50,
+ line: 60,
column: 22,
- character: 577
+ character: 647
}
},
{
code: 'css_unused_selector',
message: 'Unused CSS selector ".unused"',
start: {
- line: 69,
+ line: 79,
column: 2,
- character: 782
+ character: 852
},
end: {
- line: 69,
+ line: 79,
column: 9,
- character: 789
+ character: 859
}
},
{
code: 'css_unused_selector',
message: 'Unused CSS selector ".unused x:has(y)"',
start: {
- line: 85,
+ line: 95,
column: 1,
- character: 936
+ character: 1006
},
end: {
- line: 85,
+ line: 95,
column: 17,
- character: 952
+ character: 1022
}
},
{
code: 'css_unused_selector',
message: 'Unused CSS selector ".unused:has(.unused)"',
start: {
- line: 88,
+ line: 98,
column: 1,
- character: 973
+ character: 1043
},
end: {
- line: 88,
+ line: 98,
column: 21,
- character: 993
+ character: 1063
}
},
{
code: 'css_unused_selector',
message: 'Unused CSS selector "x:has(> z)"',
start: {
- line: 98,
+ line: 108,
column: 1,
- character: 1093
+ character: 1163
},
end: {
- line: 98,
+ line: 108,
column: 11,
- character: 1103
+ character: 1173
}
},
{
code: 'css_unused_selector',
message: 'Unused CSS selector "x:has(> d)"',
start: {
- line: 101,
+ line: 111,
column: 1,
- character: 1124
+ character: 1194
},
end: {
- line: 101,
+ line: 111,
column: 11,
- character: 1134
+ character: 1204
}
},
{
code: 'css_unused_selector',
message: 'Unused CSS selector "x:has(~ y)"',
start: {
- line: 121,
+ line: 131,
+ column: 1,
+ character: 1396
+ },
+ end: {
+ line: 131,
+ column: 11,
+ character: 1406
+ }
+ },
+ {
+ code: 'css_unused_selector',
+ message: 'Unused CSS selector "d:has(+ f)"',
+ start: {
+ line: 141,
column: 1,
- character: 1326
+ character: 1494
},
end: {
- line: 121,
+ line: 141,
column: 11,
- character: 1336
+ character: 1504
+ }
+ },
+ {
+ code: 'css_unused_selector',
+ message: 'Unused CSS selector "f:has(~ d)"',
+ start: {
+ line: 144,
+ column: 1,
+ character: 1525
+ },
+ end: {
+ line: 144,
+ column: 11,
+ character: 1535
}
},
{
code: 'css_unused_selector',
message: 'Unused CSS selector ":has(.unused)"',
start: {
- line: 129,
+ line: 152,
column: 2,
- character: 1409
+ character: 1608
},
end: {
- line: 129,
+ line: 152,
column: 15,
- character: 1422
+ character: 1621
}
},
{
code: 'css_unused_selector',
message: 'Unused CSS selector "&:has(.unused)"',
start: {
- line: 135,
+ line: 158,
column: 2,
- character: 1480
+ character: 1679
},
end: {
- line: 135,
+ line: 158,
column: 16,
- character: 1494
+ character: 1693
+ }
+ },
+ {
+ code: 'css_unused_selector',
+ message: 'Unused CSS selector ":global(.foo):has(.unused)"',
+ start: {
+ line: 166,
+ column: 1,
+ character: 1763
+ },
+ end: {
+ line: 166,
+ column: 27,
+ character: 1789
+ }
+ },
+ {
+ code: 'css_unused_selector',
+ message: 'Unused CSS selector "h:has(> h > i)"',
+ start: {
+ line: 173,
+ column: 1,
+ character: 1848
+ },
+ end: {
+ line: 173,
+ column: 15,
+ character: 1862
}
}
]
diff --git a/packages/svelte/tests/css/samples/has/expected.css b/packages/svelte/tests/css/samples/has/expected.css
index 68d6aad68a..2ce4d2bec5 100644
--- a/packages/svelte/tests/css/samples/has/expected.css
+++ b/packages/svelte/tests/css/samples/has/expected.css
@@ -112,6 +112,19 @@
color: red;
}*/
+ d.svelte-xyz:has(+ e:where(.svelte-xyz)) {
+ color: green;
+ }
+ d.svelte-xyz:has(~ f:where(.svelte-xyz)) {
+ color: green;
+ }
+ /* (unused) d:has(+ f) {
+ color: red;
+ }*/
+ /* (unused) f:has(~ d) {
+ color: red;
+ }*/
+
.foo {
.svelte-xyz:has(x:where(.svelte-xyz)) {
color: green;
@@ -126,3 +139,20 @@
color: red;
}*/
}
+
+ .foo:has(x.svelte-xyz) {
+ color: green;
+ }
+ /* (unused) :global(.foo):has(.unused) {
+ color: red;
+ }*/
+
+ g.svelte-xyz:has(> h:where(.svelte-xyz) > i:where(.svelte-xyz)) {
+ color: green;
+ }
+ /* (unused) h:has(> h > i) {
+ color: red;
+ }*/
+ g.svelte-xyz:has(+ j:where(.svelte-xyz) > k:where(.svelte-xyz)) {
+ color: green;
+ }
\ No newline at end of file
diff --git a/packages/svelte/tests/css/samples/has/input.svelte b/packages/svelte/tests/css/samples/has/input.svelte
index 3487b64e8c..033471bc16 100644
--- a/packages/svelte/tests/css/samples/has/input.svelte
+++ b/packages/svelte/tests/css/samples/has/input.svelte
@@ -3,10 +3,20 @@
{#if foo}
+
+
{/if}
+
+
+
+
+
+
+
+
diff --git a/packages/svelte/tests/css/samples/render-tag-loop/_config.js b/packages/svelte/tests/css/samples/render-tag-loop/_config.js
index f623b92cc3..292c6c49ac 100644
--- a/packages/svelte/tests/css/samples/render-tag-loop/_config.js
+++ b/packages/svelte/tests/css/samples/render-tag-loop/_config.js
@@ -1,20 +1,5 @@
import { test } from '../../test';
export default test({
- warnings: [
- {
- code: 'css_unused_selector',
- message: 'Unused CSS selector "div + div"',
- start: {
- line: 19,
- column: 1,
- character: 185
- },
- end: {
- line: 19,
- column: 10,
- character: 194
- }
- }
- ]
+ warnings: []
});
diff --git a/packages/svelte/tests/css/samples/render-tag-loop/expected.css b/packages/svelte/tests/css/samples/render-tag-loop/expected.css
index 9ced15e964..3e449286c9 100644
--- a/packages/svelte/tests/css/samples/render-tag-loop/expected.css
+++ b/packages/svelte/tests/css/samples/render-tag-loop/expected.css
@@ -2,9 +2,12 @@
div.svelte-xyz div:where(.svelte-xyz) {
color: green;
}
- /* (unused) div + div {
- color: red; /* this is marked as unused, but only because we've written an infinite loop - worth fixing? *\/
- }*/
+ div.svelte-xyz + div:where(.svelte-xyz) {
+ color: green;
+ }
div.svelte-xyz:has(div:where(.svelte-xyz)) {
color: green;
}
+ span.svelte-xyz:has(~span:where(.svelte-xyz)) {
+ color: green;
+ }
diff --git a/packages/svelte/tests/css/samples/render-tag-loop/input.svelte b/packages/svelte/tests/css/samples/render-tag-loop/input.svelte
index ade8df5744..3c55261f18 100644
--- a/packages/svelte/tests/css/samples/render-tag-loop/input.svelte
+++ b/packages/svelte/tests/css/samples/render-tag-loop/input.svelte
@@ -12,14 +12,22 @@
{/snippet}
+{#snippet c()}
+
+ {@render c()}
+{/snippet}
+
diff --git a/packages/svelte/tests/css/samples/siblings-combinator-component/Child.svelte b/packages/svelte/tests/css/samples/siblings-combinator-component/Child.svelte
new file mode 100644
index 0000000000..1df9f35e50
--- /dev/null
+++ b/packages/svelte/tests/css/samples/siblings-combinator-component/Child.svelte
@@ -0,0 +1,5 @@
+
+
+{@render foo()}
diff --git a/packages/svelte/tests/css/samples/siblings-combinator-component/_config.js b/packages/svelte/tests/css/samples/siblings-combinator-component/_config.js
new file mode 100644
index 0000000000..837fa20ae1
--- /dev/null
+++ b/packages/svelte/tests/css/samples/siblings-combinator-component/_config.js
@@ -0,0 +1,20 @@
+import { test } from '../../test';
+
+export default test({
+ warnings: [
+ {
+ code: 'css_unused_selector',
+ message: 'Unused CSS selector "n + m"',
+ end: {
+ character: 468,
+ column: 6,
+ line: 36
+ },
+ start: {
+ character: 463,
+ column: 1,
+ line: 36
+ }
+ }
+ ]
+});
diff --git a/packages/svelte/tests/css/samples/siblings-combinator-component/expected.css b/packages/svelte/tests/css/samples/siblings-combinator-component/expected.css
new file mode 100644
index 0000000000..d2657ccd21
--- /dev/null
+++ b/packages/svelte/tests/css/samples/siblings-combinator-component/expected.css
@@ -0,0 +1,8 @@
+ x.svelte-xyz + y:where(.svelte-xyz) { color: green; }
+ x.svelte-xyz + v:where(.svelte-xyz) { color: green; }
+ x.svelte-xyz + z:where(.svelte-xyz) { color: green; }
+ y.svelte-xyz + z:where(.svelte-xyz) { color: green; }
+ v.svelte-xyz + z:where(.svelte-xyz) { color: green; }
+ .component + z.svelte-xyz { color: green; }
+
+ /* (unused) n + m { color: red; }*/
diff --git a/packages/svelte/tests/css/samples/siblings-combinator-component/input.svelte b/packages/svelte/tests/css/samples/siblings-combinator-component/input.svelte
new file mode 100644
index 0000000000..8d80acffb3
--- /dev/null
+++ b/packages/svelte/tests/css/samples/siblings-combinator-component/input.svelte
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+ {#snippet foo()}
+
+ {/snippet}
+
+
+
+
+
+
+
+ {#snippet foo()}
+
+
+
+ {/snippet}
+
+
+
+
+
diff --git a/packages/svelte/tests/css/samples/siblings-combinator-slot/_config.js b/packages/svelte/tests/css/samples/siblings-combinator-slot/_config.js
index 2786baeff8..8a8f561d01 100644
--- a/packages/svelte/tests/css/samples/siblings-combinator-slot/_config.js
+++ b/packages/svelte/tests/css/samples/siblings-combinator-slot/_config.js
@@ -5,14 +5,8 @@ export default test({
{
code: 'css_unused_selector',
message: 'Unused CSS selector ".b + .c"',
- start: { character: 110, column: 1, line: 10 },
- end: { character: 117, column: 8, line: 10 }
- },
- {
- code: 'css_unused_selector',
- message: 'Unused CSS selector ".c + .f"',
- start: { character: 135, column: 1, line: 11 },
- end: { character: 142, column: 8, line: 11 }
+ start: { character: 137, column: 1, line: 11 },
+ end: { character: 144, column: 8, line: 11 }
}
]
});
diff --git a/packages/svelte/tests/css/samples/siblings-combinator-slot/expected.css b/packages/svelte/tests/css/samples/siblings-combinator-slot/expected.css
index 643f6cf13f..85cbb77e65 100644
--- a/packages/svelte/tests/css/samples/siblings-combinator-slot/expected.css
+++ b/packages/svelte/tests/css/samples/siblings-combinator-slot/expected.css
@@ -1,7 +1,7 @@
.d.svelte-xyz + .e:where(.svelte-xyz) { color: green; }
.a.svelte-xyz + .b:where(.svelte-xyz) { color: green; }
+ .c.svelte-xyz + .f:where(.svelte-xyz) { color: green; }
/* no match */
/* (unused) .b + .c { color: red; }*/
- /* (unused) .c + .f { color: red; }*/
diff --git a/packages/svelte/tests/css/samples/siblings-combinator-slot/input.svelte b/packages/svelte/tests/css/samples/siblings-combinator-slot/input.svelte
index 1b543f97b7..57e1df1507 100644
--- a/packages/svelte/tests/css/samples/siblings-combinator-slot/input.svelte
+++ b/packages/svelte/tests/css/samples/siblings-combinator-slot/input.svelte
@@ -5,10 +5,10 @@
diff --git a/packages/svelte/tests/css/samples/undefined-with-scope/expected.html b/packages/svelte/tests/css/samples/undefined-with-scope/expected.html
index ddb9429bc8..5548be6e5a 100644
--- a/packages/svelte/tests/css/samples/undefined-with-scope/expected.html
+++ b/packages/svelte/tests/css/samples/undefined-with-scope/expected.html
@@ -1 +1,2 @@
-Foo
\ No newline at end of file
+Foo
+Bar
\ No newline at end of file
diff --git a/packages/svelte/tests/css/samples/undefined-with-scope/input.svelte b/packages/svelte/tests/css/samples/undefined-with-scope/input.svelte
index c68fb40dea..20639600d0 100644
--- a/packages/svelte/tests/css/samples/undefined-with-scope/input.svelte
+++ b/packages/svelte/tests/css/samples/undefined-with-scope/input.svelte
@@ -1,3 +1,4 @@
-Foo
\ No newline at end of file
+Foo
+Bar
diff --git a/packages/svelte/tests/css/samples/view-transition/expected.css b/packages/svelte/tests/css/samples/view-transition/expected.css
index afc84d52eb..e216a4d3ad 100644
--- a/packages/svelte/tests/css/samples/view-transition/expected.css
+++ b/packages/svelte/tests/css/samples/view-transition/expected.css
@@ -8,9 +8,15 @@
::view-transition-old {
animation-duration: 0.5s;
}
+ ::view-transition-old:only-child {
+ animation-duration: 0.5s;
+ }
::view-transition-new {
animation-duration: 0.5s;
}
+ ::view-transition-new:only-child {
+ animation-duration: 0.5s;
+ }
::view-transition-image-pair {
animation-duration: 0.5s;
}
diff --git a/packages/svelte/tests/css/samples/view-transition/input.svelte b/packages/svelte/tests/css/samples/view-transition/input.svelte
index ebb2b3fd88..345213ccd3 100644
--- a/packages/svelte/tests/css/samples/view-transition/input.svelte
+++ b/packages/svelte/tests/css/samples/view-transition/input.svelte
@@ -8,9 +8,15 @@
::view-transition-old {
animation-duration: 0.5s;
}
+ ::view-transition-old:only-child {
+ animation-duration: 0.5s;
+ }
::view-transition-new {
animation-duration: 0.5s;
}
+ ::view-transition-new:only-child {
+ animation-duration: 0.5s;
+ }
::view-transition-image-pair {
animation-duration: 0.5s;
}
diff --git a/packages/svelte/tests/css/test.ts b/packages/svelte/tests/css/test.ts
index dd51f52eab..8846b1d986 100644
--- a/packages/svelte/tests/css/test.ts
+++ b/packages/svelte/tests/css/test.ts
@@ -34,6 +34,7 @@ interface CssTest extends BaseTest {
compileOptions?: Partial;
warnings?: Warning[];
props?: Record;
+ hasGlobal?: boolean;
}
/**
@@ -78,6 +79,14 @@ const { test, run } = suite(async (config, cwd) => {
// assert_html_equal(actual_ssr, expected.html);
}
+ if (config.hasGlobal !== undefined) {
+ const metadata = JSON.parse(
+ fs.readFileSync(`${cwd}/_output/client/input.svelte.css.json`, 'utf-8')
+ );
+
+ assert.equal(metadata.hasGlobal, config.hasGlobal);
+ }
+
const dom_css = fs.readFileSync(`${cwd}/_output/client/input.svelte.css`, 'utf-8').trim();
const ssr_css = fs.readFileSync(`${cwd}/_output/server/input.svelte.css`, 'utf-8').trim();
diff --git a/packages/svelte/tests/helpers.js b/packages/svelte/tests/helpers.js
index 9d7f71c9bd..f853d5873c 100644
--- a/packages/svelte/tests/helpers.js
+++ b/packages/svelte/tests/helpers.js
@@ -1,7 +1,7 @@
/** @import { CompileOptions } from '#compiler' */
import * as fs from 'node:fs';
import * as path from 'node:path';
-import glob from 'tiny-glob/sync.js';
+import { globSync } from 'tinyglobby';
import { VERSION, compile, compileModule, preprocess } from 'svelte/compiler';
import { vi } from 'vitest';
@@ -70,7 +70,7 @@ export async function compile_directory(
fs.rmSync(output_dir, { recursive: true, force: true });
- for (let file of glob('**', { cwd, filesOnly: true })) {
+ for (let file of globSync('**', { cwd, onlyFiles: true })) {
if (file.startsWith('_')) continue;
let text = fs.readFileSync(`${cwd}/${file}`, 'utf-8').replace(/\r\n/g, '\n');
@@ -146,6 +146,10 @@ export async function compile_directory(
if (compiled.css) {
write(`${output_dir}/${file}.css`, compiled.css.code);
+ write(
+ `${output_dir}/${file}.css.json`,
+ JSON.stringify({ hasGlobal: compiled.css.hasGlobal })
+ );
if (output_map) {
write(`${output_dir}/${file}.css.map`, JSON.stringify(compiled.css.map, null, '\t'));
}
diff --git a/packages/svelte/tests/html_equal.js b/packages/svelte/tests/html_equal.js
index 0ebf1fa6bd..4c9e2a7253 100644
--- a/packages/svelte/tests/html_equal.js
+++ b/packages/svelte/tests/html_equal.js
@@ -86,7 +86,7 @@ export function normalize_html(
clean_children(node);
return node.innerHTML;
} catch (err) {
- throw new Error(`Failed to normalize HTML:\n${html}`);
+ throw new Error(`Failed to normalize HTML:\n${html}\nCause: ${err}`);
}
}
diff --git a/packages/svelte/tests/hydration/samples/element-dir-attribute-sibling/_config.js b/packages/svelte/tests/hydration/samples/element-dir-attribute-sibling/_config.js
new file mode 100644
index 0000000000..e3c629aef9
--- /dev/null
+++ b/packages/svelte/tests/hydration/samples/element-dir-attribute-sibling/_config.js
@@ -0,0 +1,9 @@
+import { test } from '../../test';
+
+export default test({
+ test(assert, target) {
+ const p = target.querySelector('p');
+
+ assert.equal(p?.dir, 'rtl');
+ }
+});
diff --git a/packages/svelte/tests/hydration/samples/element-dir-attribute-sibling/main.svelte b/packages/svelte/tests/hydration/samples/element-dir-attribute-sibling/main.svelte
new file mode 100644
index 0000000000..802edc0fee
--- /dev/null
+++ b/packages/svelte/tests/hydration/samples/element-dir-attribute-sibling/main.svelte
@@ -0,0 +1 @@
+text
.
diff --git a/packages/svelte/tests/hydration/samples/input-checked-attribute-sibling/_config.js b/packages/svelte/tests/hydration/samples/input-checked-attribute-sibling/_config.js
new file mode 100644
index 0000000000..31ec66fc88
--- /dev/null
+++ b/packages/svelte/tests/hydration/samples/input-checked-attribute-sibling/_config.js
@@ -0,0 +1,9 @@
+import { test } from '../../test';
+
+export default test({
+ test(assert, target) {
+ const input = target.querySelector('input');
+
+ assert.equal(input?.checked, true);
+ }
+});
diff --git a/packages/svelte/tests/hydration/samples/input-checked-attribute-sibling/_expected.html b/packages/svelte/tests/hydration/samples/input-checked-attribute-sibling/_expected.html
new file mode 100644
index 0000000000..bcd53f8783
--- /dev/null
+++ b/packages/svelte/tests/hydration/samples/input-checked-attribute-sibling/_expected.html
@@ -0,0 +1 @@
+.
diff --git a/packages/svelte/tests/hydration/samples/input-checked-attribute-sibling/main.svelte b/packages/svelte/tests/hydration/samples/input-checked-attribute-sibling/main.svelte
new file mode 100644
index 0000000000..db3eae870f
--- /dev/null
+++ b/packages/svelte/tests/hydration/samples/input-checked-attribute-sibling/main.svelte
@@ -0,0 +1 @@
+.
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/hydration/test.ts b/packages/svelte/tests/hydration/test.ts
index 3bf2dd286c..266ac07bff 100644
--- a/packages/svelte/tests/hydration/test.ts
+++ b/packages/svelte/tests/hydration/test.ts
@@ -2,9 +2,9 @@
import * as fs from 'node:fs';
import { assert } from 'vitest';
-import { compile_directory, should_update_expected } from '../helpers.js';
+import { compile_directory } from '../helpers.js';
import { assert_html_equal } from '../html_equal.js';
-import { suite, assert_ok, type BaseTest } from '../suite.js';
+import { assert_ok, suite, type BaseTest } from '../suite.js';
import { createClassComponent } from 'svelte/legacy';
import { render } from 'svelte/server';
import type { CompileOptions } from '#compiler';
@@ -13,6 +13,7 @@ import { flushSync } from 'svelte';
interface HydrationTest extends BaseTest {
load_compiled?: boolean;
server_props?: Record;
+ id_prefix?: string;
props?: Record;
compileOptions?: Partial;
/**
@@ -50,7 +51,8 @@ const { test, run } = suite(async (config, cwd) => {
const head = window.document.head;
const rendered = render((await import(`${cwd}/_output/server/main.svelte.js`)).default, {
- props: config.server_props ?? config.props ?? {}
+ props: config.server_props ?? config.props ?? {},
+ idPrefix: config?.id_prefix
});
const override = read(`${cwd}/_override.html`);
@@ -103,7 +105,8 @@ const { test, run } = suite(async (config, cwd) => {
component: (await import(`${cwd}/_output/client/main.svelte.js`)).default,
target,
hydrate: true,
- props: config.props
+ props: config.props,
+ idPrefix: config?.id_prefix
});
console.warn = warn;
diff --git a/packages/svelte/tests/migrate/samples/jsdoc-with-comments/input.svelte b/packages/svelte/tests/migrate/samples/jsdoc-with-comments/input.svelte
index f2efb1db80..f138c3a070 100644
--- a/packages/svelte/tests/migrate/samples/jsdoc-with-comments/input.svelte
+++ b/packages/svelte/tests/migrate/samples/jsdoc-with-comments/input.svelte
@@ -21,6 +21,9 @@
*/
export let type_no_comment;
+ /** @type {boolean} type_with_comment - One-line declaration with comment */
+ export let type_with_comment;
+
/**
* This is optional
*/
@@ -40,4 +43,10 @@
export let inline_multiline_trailing_comment = 'world'; /*
* this is a same-line trailing multiline comment
**/
+
+ /** @type {number} [default_value=1] */
+ export let default_value = 1;
+
+ /** @type {number} [comment_default_value=1] - This has a comment and an optional value. */
+ export let comment_default_value = 1;
\ No newline at end of file
diff --git a/packages/svelte/tests/migrate/samples/jsdoc-with-comments/output.svelte b/packages/svelte/tests/migrate/samples/jsdoc-with-comments/output.svelte
index 19fbe38b50..32133ccd4c 100644
--- a/packages/svelte/tests/migrate/samples/jsdoc-with-comments/output.svelte
+++ b/packages/svelte/tests/migrate/samples/jsdoc-with-comments/output.svelte
@@ -9,12 +9,18 @@
+
+
+
+
+
+
/**
* @typedef {Object} Props
* @property {string} comment - My wonderful comment
@@ -22,11 +28,14 @@
* @property {any} one_line - one line comment
* @property {any} no_comment
* @property {boolean} type_no_comment
+ * @property {boolean} type_with_comment - One-line declaration with comment
* @property {any} [optional] - This is optional
* @property {any} inline_commented - this should stay a comment
* @property {any} inline_commented_merged - This comment should be merged - with this inline comment
* @property {string} [inline_multiline_leading_comment] - this is a same-line leading multiline comment
* @property {string} [inline_multiline_trailing_comment] - this is a same-line trailing multiline comment
+ * @property {number} [default_value]
+ * @property {number} [comment_default_value] - This has a comment and an optional value.
*/
/** @type {Props} */
@@ -36,10 +45,13 @@
one_line,
no_comment,
type_no_comment,
+ type_with_comment,
optional = {stuff: true},
inline_commented,
inline_commented_merged,
inline_multiline_leading_comment = 'world',
- inline_multiline_trailing_comment = 'world'
+ inline_multiline_trailing_comment = 'world',
+ default_value = 1,
+ comment_default_value = 1
} = $props();
\ No newline at end of file
diff --git a/packages/svelte/tests/migrate/samples/labeled-statement-reassign-state/input.svelte b/packages/svelte/tests/migrate/samples/labeled-statement-reassign-state/input.svelte
new file mode 100644
index 0000000000..0b5c13d889
--- /dev/null
+++ b/packages/svelte/tests/migrate/samples/labeled-statement-reassign-state/input.svelte
@@ -0,0 +1,6 @@
+
\ No newline at end of file
diff --git a/packages/svelte/tests/migrate/samples/labeled-statement-reassign-state/output.svelte b/packages/svelte/tests/migrate/samples/labeled-statement-reassign-state/output.svelte
new file mode 100644
index 0000000000..c2b36a6e30
--- /dev/null
+++ b/packages/svelte/tests/migrate/samples/labeled-statement-reassign-state/output.svelte
@@ -0,0 +1,10 @@
+
\ No newline at end of file
diff --git a/packages/svelte/tests/migrate/samples/reassigned-deriveds/input.svelte b/packages/svelte/tests/migrate/samples/reassigned-deriveds/input.svelte
new file mode 100644
index 0000000000..024f719fb9
--- /dev/null
+++ b/packages/svelte/tests/migrate/samples/reassigned-deriveds/input.svelte
@@ -0,0 +1,10 @@
+
+
+
+
+
+{upper}
\ No newline at end of file
diff --git a/packages/svelte/tests/migrate/samples/reassigned-deriveds/output.svelte b/packages/svelte/tests/migrate/samples/reassigned-deriveds/output.svelte
new file mode 100644
index 0000000000..0903299d95
--- /dev/null
+++ b/packages/svelte/tests/migrate/samples/reassigned-deriveds/output.svelte
@@ -0,0 +1,10 @@
+
+
+
+
+
+{upper}
\ No newline at end of file
diff --git a/packages/svelte/tests/parser-modern/samples/comment-before-function-binding/input.svelte b/packages/svelte/tests/parser-modern/samples/comment-before-function-binding/input.svelte
new file mode 100644
index 0000000000..50200a9eac
--- /dev/null
+++ b/packages/svelte/tests/parser-modern/samples/comment-before-function-binding/input.svelte
@@ -0,0 +1,10 @@
+
+
+ value,
+ (v) => value = v.toLowerCase()
+}
+/>
diff --git a/packages/svelte/tests/parser-modern/samples/comment-before-function-binding/output.json b/packages/svelte/tests/parser-modern/samples/comment-before-function-binding/output.json
new file mode 100644
index 0000000000..dba258a6b1
--- /dev/null
+++ b/packages/svelte/tests/parser-modern/samples/comment-before-function-binding/output.json
@@ -0,0 +1,326 @@
+{
+ "css": null,
+ "js": [],
+ "start": 37,
+ "end": 117,
+ "type": "Root",
+ "fragment": {
+ "type": "Fragment",
+ "nodes": [
+ {
+ "type": "Text",
+ "start": 35,
+ "end": 37,
+ "raw": "\n\n",
+ "data": "\n\n"
+ },
+ {
+ "type": "RegularElement",
+ "start": 37,
+ "end": 117,
+ "name": "input",
+ "attributes": [
+ {
+ "start": 44,
+ "end": 114,
+ "type": "BindDirective",
+ "name": "value",
+ "expression": {
+ "type": "SequenceExpression",
+ "start": 68,
+ "end": 112,
+ "loc": {
+ "start": {
+ "line": 7,
+ "column": 1
+ },
+ "end": {
+ "line": 8,
+ "column": 31
+ }
+ },
+ "expressions": [
+ {
+ "type": "ArrowFunctionExpression",
+ "start": 68,
+ "end": 79,
+ "loc": {
+ "start": {
+ "line": 7,
+ "column": 1
+ },
+ "end": {
+ "line": 7,
+ "column": 12
+ }
+ },
+ "id": null,
+ "expression": true,
+ "generator": false,
+ "async": false,
+ "params": [],
+ "body": {
+ "type": "Identifier",
+ "start": 74,
+ "end": 79,
+ "loc": {
+ "start": {
+ "line": 7,
+ "column": 7
+ },
+ "end": {
+ "line": 7,
+ "column": 12
+ }
+ },
+ "name": "value"
+ }
+ },
+ {
+ "type": "ArrowFunctionExpression",
+ "start": 82,
+ "end": 112,
+ "loc": {
+ "start": {
+ "line": 8,
+ "column": 1
+ },
+ "end": {
+ "line": 8,
+ "column": 31
+ }
+ },
+ "id": null,
+ "expression": true,
+ "generator": false,
+ "async": false,
+ "params": [
+ {
+ "type": "Identifier",
+ "start": 83,
+ "end": 84,
+ "loc": {
+ "start": {
+ "line": 8,
+ "column": 2
+ },
+ "end": {
+ "line": 8,
+ "column": 3
+ }
+ },
+ "name": "v"
+ }
+ ],
+ "body": {
+ "type": "AssignmentExpression",
+ "start": 89,
+ "end": 112,
+ "loc": {
+ "start": {
+ "line": 8,
+ "column": 8
+ },
+ "end": {
+ "line": 8,
+ "column": 31
+ }
+ },
+ "operator": "=",
+ "left": {
+ "type": "Identifier",
+ "start": 89,
+ "end": 94,
+ "loc": {
+ "start": {
+ "line": 8,
+ "column": 8
+ },
+ "end": {
+ "line": 8,
+ "column": 13
+ }
+ },
+ "name": "value"
+ },
+ "right": {
+ "type": "CallExpression",
+ "start": 97,
+ "end": 112,
+ "loc": {
+ "start": {
+ "line": 8,
+ "column": 16
+ },
+ "end": {
+ "line": 8,
+ "column": 31
+ }
+ },
+ "callee": {
+ "type": "MemberExpression",
+ "start": 97,
+ "end": 110,
+ "loc": {
+ "start": {
+ "line": 8,
+ "column": 16
+ },
+ "end": {
+ "line": 8,
+ "column": 29
+ }
+ },
+ "object": {
+ "type": "Identifier",
+ "start": 97,
+ "end": 98,
+ "loc": {
+ "start": {
+ "line": 8,
+ "column": 16
+ },
+ "end": {
+ "line": 8,
+ "column": 17
+ }
+ },
+ "name": "v"
+ },
+ "property": {
+ "type": "Identifier",
+ "start": 99,
+ "end": 110,
+ "loc": {
+ "start": {
+ "line": 8,
+ "column": 18
+ },
+ "end": {
+ "line": 8,
+ "column": 29
+ }
+ },
+ "name": "toLowerCase"
+ },
+ "computed": false,
+ "optional": false
+ },
+ "arguments": [],
+ "optional": false
+ }
+ }
+ }
+ ],
+ "leadingComments": [
+ {
+ "type": "Block",
+ "value": "* ( ",
+ "start": 58,
+ "end": 66
+ }
+ ]
+ },
+ "modifiers": []
+ }
+ ],
+ "fragment": {
+ "type": "Fragment",
+ "nodes": []
+ }
+ }
+ ]
+ },
+ "options": null,
+ "instance": {
+ "type": "Script",
+ "start": 0,
+ "end": 35,
+ "context": "default",
+ "content": {
+ "type": "Program",
+ "start": 8,
+ "end": 26,
+ "loc": {
+ "start": {
+ "line": 1,
+ "column": 0
+ },
+ "end": {
+ "line": 3,
+ "column": 0
+ }
+ },
+ "body": [
+ {
+ "type": "VariableDeclaration",
+ "start": 10,
+ "end": 25,
+ "loc": {
+ "start": {
+ "line": 2,
+ "column": 1
+ },
+ "end": {
+ "line": 2,
+ "column": 16
+ }
+ },
+ "declarations": [
+ {
+ "type": "VariableDeclarator",
+ "start": 14,
+ "end": 24,
+ "loc": {
+ "start": {
+ "line": 2,
+ "column": 5
+ },
+ "end": {
+ "line": 2,
+ "column": 15
+ }
+ },
+ "id": {
+ "type": "Identifier",
+ "start": 14,
+ "end": 19,
+ "loc": {
+ "start": {
+ "line": 2,
+ "column": 5
+ },
+ "end": {
+ "line": 2,
+ "column": 10
+ }
+ },
+ "name": "value"
+ },
+ "init": {
+ "type": "Literal",
+ "start": 22,
+ "end": 24,
+ "loc": {
+ "start": {
+ "line": 2,
+ "column": 13
+ },
+ "end": {
+ "line": 2,
+ "column": 15
+ }
+ },
+ "value": "",
+ "raw": "''"
+ }
+ }
+ ],
+ "kind": "let"
+ }
+ ],
+ "sourceType": "module"
+ },
+ "attributes": []
+ }
+}
diff --git a/packages/svelte/tests/parser-modern/samples/comment-before-script/output.json b/packages/svelte/tests/parser-modern/samples/comment-before-script/output.json
index 13c099a857..1aca0ce036 100644
--- a/packages/svelte/tests/parser-modern/samples/comment-before-script/output.json
+++ b/packages/svelte/tests/parser-modern/samples/comment-before-script/output.json
@@ -75,7 +75,7 @@
"id": {
"type": "Identifier",
"start": 52,
- "end": 57,
+ "end": 65,
"loc": {
"start": {
"line": 3,
diff --git a/packages/svelte/tests/parser-modern/samples/snippets/output.json b/packages/svelte/tests/parser-modern/samples/snippets/output.json
index 2cf2596b18..acf484d8ae 100644
--- a/packages/svelte/tests/parser-modern/samples/snippets/output.json
+++ b/packages/svelte/tests/parser-modern/samples/snippets/output.json
@@ -28,7 +28,7 @@
{
"type": "Identifier",
"start": 43,
- "end": 46,
+ "end": 54,
"loc": {
"start": {
"line": 3,
diff --git a/packages/svelte/tests/parser-modern/samples/typescript-in-event-handler/output.json b/packages/svelte/tests/parser-modern/samples/typescript-in-event-handler/output.json
index 5d72e01b0a..9c515ad905 100644
--- a/packages/svelte/tests/parser-modern/samples/typescript-in-event-handler/output.json
+++ b/packages/svelte/tests/parser-modern/samples/typescript-in-event-handler/output.json
@@ -25,7 +25,6 @@
"end": 147,
"type": "OnDirective",
"name": "click",
- "modifiers": [],
"expression": {
"type": "ArrowFunctionExpression",
"start": 73,
@@ -48,7 +47,7 @@
{
"type": "Identifier",
"start": 74,
- "end": 75,
+ "end": 87,
"loc": {
"start": {
"line": 6,
@@ -155,7 +154,7 @@
"id": {
"type": "Identifier",
"start": 102,
- "end": 106,
+ "end": 114,
"loc": {
"start": {
"line": 7,
@@ -316,7 +315,8 @@
}
]
}
- }
+ },
+ "modifiers": []
}
],
"fragment": {
diff --git a/packages/svelte/tests/preprocess/samples/script/expected_map.json b/packages/svelte/tests/preprocess/samples/script/expected_map.json
index d5bf98483f..22275b5338 100644
--- a/packages/svelte/tests/preprocess/samples/script/expected_map.json
+++ b/packages/svelte/tests/preprocess/samples/script/expected_map.json
@@ -1,6 +1,6 @@
{
"version": 3,
- "mappings": "AAAA,CAAC,MAAM,CAAC;AACR,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAc,CAAC,CAAC;AAC7B,CAAC,CAAC,MAAM",
+ "mappings": "AAAA,CAAC,MAAM;AACP,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAc,CAAC;AAC5B,CAAC,CAAC,MAAM",
"names": [],
"sources": [
"input.svelte"
diff --git a/packages/svelte/tests/preprocess/test.ts b/packages/svelte/tests/preprocess/test.ts
index 08f237e286..81030a6346 100644
--- a/packages/svelte/tests/preprocess/test.ts
+++ b/packages/svelte/tests/preprocess/test.ts
@@ -25,7 +25,7 @@ const { test, run } = suite(async (config, cwd) => {
fs.writeFileSync(`${cwd}/_actual.html.map`, JSON.stringify(result.map, null, 2));
}
- expect(result.code).toMatchFileSnapshot(`${cwd}/output.svelte`);
+ await expect(result.code).toMatchFileSnapshot(`${cwd}/output.svelte`);
expect(result.dependencies).toEqual(config.dependencies || []);
diff --git a/packages/svelte/tests/runtime-browser/assert.js b/packages/svelte/tests/runtime-browser/assert.js
index 249d19f809..fb460c722a 100644
--- a/packages/svelte/tests/runtime-browser/assert.js
+++ b/packages/svelte/tests/runtime-browser/assert.js
@@ -119,6 +119,7 @@ function normalize_children(node) {
* skip_mode?: Array<'server' | 'client' | 'hydrate'>;
* html?: string;
* ssrHtml?: string;
+ * id_prefix?: string;
* props?: Props;
* compileOptions?: Partial;
* test?: (args: {
diff --git a/packages/svelte/tests/runtime-browser/custom-elements-samples/camel-case-attribute/_config.js b/packages/svelte/tests/runtime-browser/custom-elements-samples/camel-case-attribute/_config.js
index ba3be3c25b..b9bfdd7782 100644
--- a/packages/svelte/tests/runtime-browser/custom-elements-samples/camel-case-attribute/_config.js
+++ b/packages/svelte/tests/runtime-browser/custom-elements-samples/camel-case-attribute/_config.js
@@ -1,3 +1,4 @@
+import { flushSync } from 'svelte';
import { test } from '../../assert';
const tick = () => Promise.resolve();
@@ -14,8 +15,7 @@ export default test({
el.setAttribute('camel-case', 'universe');
el.setAttribute('an-array', '[3,4]');
el.setAttribute('camelcase2', 'Hi');
- await tick();
- await tick();
+ flushSync();
assert.htmlEqual(el.shadowRoot.innerHTML, 'Hi universe! 3
4
');
assert.htmlEqual(
target.innerHTML,
@@ -25,8 +25,7 @@ export default test({
el.camelCase = 'galaxy';
el.camelCase2 = 'Hey';
el.anArray = [5, 6];
- await tick();
- await tick();
+ flushSync();
assert.htmlEqual(el.shadowRoot.innerHTML, 'Hey galaxy! 5
6
');
assert.htmlEqual(
target.innerHTML,
diff --git a/packages/svelte/tests/runtime-browser/custom-elements-samples/custom-method/_config.js b/packages/svelte/tests/runtime-browser/custom-elements-samples/custom-method/_config.js
index dbdf006be5..4314926a94 100644
--- a/packages/svelte/tests/runtime-browser/custom-elements-samples/custom-method/_config.js
+++ b/packages/svelte/tests/runtime-browser/custom-elements-samples/custom-method/_config.js
@@ -1,3 +1,4 @@
+import { flushSync } from 'svelte';
import { test } from '../../assert';
const tick = () => Promise.resolve();
@@ -8,7 +9,8 @@ export default test({
/** @type {any} */
const el = target.querySelector('custom-element');
- await el.updateFoo(42);
+ el.updateFoo(42);
+ flushSync();
const p = el.shadowRoot.querySelector('p');
assert.equal(p.textContent, '42');
diff --git a/packages/svelte/tests/runtime-browser/custom-elements-samples/element-effect-context/_config.js b/packages/svelte/tests/runtime-browser/custom-elements-samples/element-effect-context/_config.js
index 3cf7a66df1..7f2ba9f331 100644
--- a/packages/svelte/tests/runtime-browser/custom-elements-samples/element-effect-context/_config.js
+++ b/packages/svelte/tests/runtime-browser/custom-elements-samples/element-effect-context/_config.js
@@ -1,4 +1,5 @@
import { test } from '../../assert';
+import { flushSync } from 'svelte';
const tick = () => Promise.resolve();
export default test({
@@ -16,7 +17,7 @@ export default test({
assert.equal(p.innerHTML, 'false');
button.click();
- await tick();
+ flushSync();
assert.equal(button.innerHTML, '1');
assert.equal(p.innerHTML, 'false');
diff --git a/packages/svelte/tests/runtime-browser/driver-ssr.js b/packages/svelte/tests/runtime-browser/driver-ssr.js
index f5f15b6493..7067e48a1f 100644
--- a/packages/svelte/tests/runtime-browser/driver-ssr.js
+++ b/packages/svelte/tests/runtime-browser/driver-ssr.js
@@ -6,5 +6,5 @@ import config from '__CONFIG__';
import { render } from 'svelte/server';
export default function () {
- return render(SvelteComponent, { props: config.props || {} });
+ return render(SvelteComponent, { props: config.props || {}, idPrefix: config?.id_prefix });
}
diff --git a/packages/svelte/tests/runtime-browser/test-ssr.ts b/packages/svelte/tests/runtime-browser/test-ssr.ts
index 2ff1659f80..6987fac915 100644
--- a/packages/svelte/tests/runtime-browser/test-ssr.ts
+++ b/packages/svelte/tests/runtime-browser/test-ssr.ts
@@ -20,7 +20,7 @@ export async function run_ssr_test(
await compile_directory(test_dir, 'server', config.compileOptions);
const Component = (await import(`${test_dir}/_output/server/main.svelte.js`)).default;
- const { body } = render(Component, { props: config.props || {} });
+ const { body } = render(Component, { props: config.props || {}, idPrefix: config.id_prefix });
fs.writeFileSync(`${test_dir}/_output/rendered.html`, body);
diff --git a/packages/svelte/tests/runtime-legacy/samples/action-custom-event-handler-in-each-destructured/_config.js b/packages/svelte/tests/runtime-legacy/samples/action-custom-event-handler-in-each-destructured/_config.js
index 650d0ec360..fe13d43bc8 100644
--- a/packages/svelte/tests/runtime-legacy/samples/action-custom-event-handler-in-each-destructured/_config.js
+++ b/packages/svelte/tests/runtime-legacy/samples/action-custom-event-handler-in-each-destructured/_config.js
@@ -1,3 +1,4 @@
+import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
@@ -10,12 +11,13 @@ export default test({
second:
`,
- async test({ assert, component, target, window }) {
+ test({ assert, component, target, window }) {
const event = new window.MouseEvent('click');
const buttons = target.querySelectorAll('button');
- await buttons[1].dispatchEvent(event);
+ buttons[1].dispatchEvent(event);
+ flushSync();
assert.htmlEqual(
target.innerHTML,
diff --git a/packages/svelte/tests/runtime-legacy/samples/action-custom-event-handler-in-each/_config.js b/packages/svelte/tests/runtime-legacy/samples/action-custom-event-handler-in-each/_config.js
index 21fb167873..83b14cb8a3 100644
--- a/packages/svelte/tests/runtime-legacy/samples/action-custom-event-handler-in-each/_config.js
+++ b/packages/svelte/tests/runtime-legacy/samples/action-custom-event-handler-in-each/_config.js
@@ -1,3 +1,4 @@
+import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
@@ -10,12 +11,13 @@ export default test({
fromState:
`,
- async test({ assert, component, target, window }) {
+ test({ assert, component, target, window }) {
const event = new window.MouseEvent('click');
const buttons = target.querySelectorAll('button');
- await buttons[1].dispatchEvent(event);
+ buttons[1].dispatchEvent(event);
+ flushSync();
assert.htmlEqual(
target.innerHTML,
diff --git a/packages/svelte/tests/runtime-legacy/samples/action-custom-event-handler-node-context/_config.js b/packages/svelte/tests/runtime-legacy/samples/action-custom-event-handler-node-context/_config.js
index a2edd90f8b..9276ca8a59 100644
--- a/packages/svelte/tests/runtime-legacy/samples/action-custom-event-handler-node-context/_config.js
+++ b/packages/svelte/tests/runtime-legacy/samples/action-custom-event-handler-node-context/_config.js
@@ -1,3 +1,4 @@
+import { flushSync } from 'svelte';
import { ok, test } from '../../test';
export default test({
@@ -5,13 +6,14 @@ export default test({
html: '10 ',
- async test({ assert, target, window }) {
+ test({ assert, target, window }) {
const event = new window.MouseEvent('click');
const button = target.querySelector('button');
ok(button);
- await button.dispatchEvent(event);
+ button.dispatchEvent(event);
+ flushSync();
assert.htmlEqual(target.innerHTML, '11 ');
}
diff --git a/packages/svelte/tests/runtime-legacy/samples/action-custom-event-handler-with-context/_config.js b/packages/svelte/tests/runtime-legacy/samples/action-custom-event-handler-with-context/_config.js
index c4d84fc2b3..2e6aa4dfee 100644
--- a/packages/svelte/tests/runtime-legacy/samples/action-custom-event-handler-with-context/_config.js
+++ b/packages/svelte/tests/runtime-legacy/samples/action-custom-event-handler-with-context/_config.js
@@ -1,9 +1,10 @@
+import { flushSync } from 'svelte';
import { ok, test } from '../../test';
export default test({
html: '??? ',
- async test({ assert, target, window }) {
+ test({ assert, target, window }) {
const event = new window.MouseEvent('click', {
clientX: 42,
clientY: 42
@@ -12,7 +13,8 @@ export default test({
const button = target.querySelector('button');
ok(button);
- await button.dispatchEvent(event);
+ button.dispatchEvent(event);
+ flushSync();
assert.htmlEqual(target.innerHTML, '42 ');
}
diff --git a/packages/svelte/tests/runtime-legacy/samples/action-custom-event-handler/_config.js b/packages/svelte/tests/runtime-legacy/samples/action-custom-event-handler/_config.js
index 279171692d..3769a8bd81 100644
--- a/packages/svelte/tests/runtime-legacy/samples/action-custom-event-handler/_config.js
+++ b/packages/svelte/tests/runtime-legacy/samples/action-custom-event-handler/_config.js
@@ -1,9 +1,10 @@
+import { flushSync } from 'svelte';
import { ok, test } from '../../test';
export default test({
html: '0, 0 ',
- async test({ assert, target, window }) {
+ test({ assert, target, window }) {
const event = new window.MouseEvent('click', {
clientX: 42,
clientY: 42
@@ -12,7 +13,8 @@ export default test({
const button = target.querySelector('button');
ok(button);
- await button.dispatchEvent(event);
+ button.dispatchEvent(event);
+ flushSync();
assert.htmlEqual(target.innerHTML, '42, 42 ');
}
diff --git a/packages/svelte/tests/runtime-legacy/samples/action-this/_config.js b/packages/svelte/tests/runtime-legacy/samples/action-this/_config.js
index d296fc5a28..0ab9305ade 100644
--- a/packages/svelte/tests/runtime-legacy/samples/action-this/_config.js
+++ b/packages/svelte/tests/runtime-legacy/samples/action-this/_config.js
@@ -1,3 +1,4 @@
+import { flushSync } from 'svelte';
import { ok, test } from '../../test';
export default test({
@@ -8,7 +9,8 @@ export default test({
const click = new window.MouseEvent('click');
assert.htmlEqual(target.innerHTML, '1 ');
- await button.dispatchEvent(click);
+ button.dispatchEvent(click);
+ flushSync();
assert.htmlEqual(target.innerHTML, '2 ');
}
});
diff --git a/packages/svelte/tests/runtime-legacy/samples/animation-css/_config.js b/packages/svelte/tests/runtime-legacy/samples/animation-css/_config.js
index b6b601a96b..b6bd818e65 100644
--- a/packages/svelte/tests/runtime-legacy/samples/animation-css/_config.js
+++ b/packages/svelte/tests/runtime-legacy/samples/animation-css/_config.js
@@ -47,6 +47,8 @@ export default test({
{ id: 1, name: 'a' }
];
+ raf.tick(0);
+
divs = target.querySelectorAll('div');
assert.ok(divs[0].getAnimations().length > 0);
assert.equal(divs[1].getAnimations().length, 0);
diff --git a/packages/svelte/tests/runtime-legacy/samples/animation-js-easing/_config.js b/packages/svelte/tests/runtime-legacy/samples/animation-js-easing/_config.js
index 5b7ed1c732..f4a3554b29 100644
--- a/packages/svelte/tests/runtime-legacy/samples/animation-js-easing/_config.js
+++ b/packages/svelte/tests/runtime-legacy/samples/animation-js-easing/_config.js
@@ -46,6 +46,8 @@ export default test({
{ id: 1, name: 'a' }
];
+ raf.tick(0);
+
divs = document.querySelectorAll('div');
assert.equal(divs[0].dy, 120);
assert.equal(divs[4].dy, -120);
diff --git a/packages/svelte/tests/runtime-legacy/samples/animation-js/_config.js b/packages/svelte/tests/runtime-legacy/samples/animation-js/_config.js
index 3606f7d17b..a2e17b49f8 100644
--- a/packages/svelte/tests/runtime-legacy/samples/animation-js/_config.js
+++ b/packages/svelte/tests/runtime-legacy/samples/animation-js/_config.js
@@ -46,6 +46,8 @@ export default test({
{ id: 1, name: 'a' }
];
+ raf.tick(0);
+
divs = document.querySelectorAll('div');
assert.equal(divs[0].dy, 120);
assert.equal(divs[4].dy, -120);
@@ -66,6 +68,8 @@ export default test({
{ id: 5, name: 'e' }
];
+ raf.tick(100);
+
divs = document.querySelectorAll('div');
assert.equal(divs[0].dy, 120);
diff --git a/packages/svelte/tests/runtime-legacy/samples/attribute-null-classname-with-style/_config.js b/packages/svelte/tests/runtime-legacy/samples/attribute-null-classname-with-style/_config.js
index c8710f9038..cbd0456e13 100644
--- a/packages/svelte/tests/runtime-legacy/samples/attribute-null-classname-with-style/_config.js
+++ b/packages/svelte/tests/runtime-legacy/samples/attribute-null-classname-with-style/_config.js
@@ -1,17 +1,17 @@
import { ok, test } from '../../test';
export default test({
- html: '
',
+ html: '
',
test({ assert, component, target }) {
const div = target.querySelector('div');
ok(div);
component.testName = null;
- assert.equal(div.className, ' svelte-x1o6ra');
+ assert.equal(div.className, 'svelte-x1o6ra');
component.testName = undefined;
- assert.equal(div.className, ' svelte-x1o6ra');
+ assert.equal(div.className, 'svelte-x1o6ra');
component.testName = undefined + '';
assert.equal(div.className, 'undefined svelte-x1o6ra');
@@ -32,10 +32,10 @@ export default test({
assert.equal(div.className, 'true svelte-x1o6ra');
component.testName = {};
- assert.equal(div.className, ' svelte-x1o6ra');
+ assert.equal(div.className, 'svelte-x1o6ra');
component.testName = '';
- assert.equal(div.className, ' svelte-x1o6ra');
+ assert.equal(div.className, 'svelte-x1o6ra');
component.testName = 'testClassName';
assert.equal(div.className, 'testClassName svelte-x1o6ra');
diff --git a/packages/svelte/tests/runtime-legacy/samples/attribute-null-func-classname-with-style/_config.js b/packages/svelte/tests/runtime-legacy/samples/attribute-null-func-classname-with-style/_config.js
index 8d0f411b8f..081fceecf2 100644
--- a/packages/svelte/tests/runtime-legacy/samples/attribute-null-func-classname-with-style/_config.js
+++ b/packages/svelte/tests/runtime-legacy/samples/attribute-null-func-classname-with-style/_config.js
@@ -16,10 +16,10 @@ export default test({
assert.equal(div.className, 'testClassName svelte-x1o6ra');
component.testName = null;
- assert.equal(div.className, ' svelte-x1o6ra');
+ assert.equal(div.className, 'svelte-x1o6ra');
component.testName = undefined;
- assert.equal(div.className, ' svelte-x1o6ra');
+ assert.equal(div.className, 'svelte-x1o6ra');
component.testName = undefined + '';
assert.equal(div.className, 'undefined svelte-x1o6ra');
@@ -40,9 +40,9 @@ export default test({
assert.equal(div.className, 'true svelte-x1o6ra');
component.testName = {};
- assert.equal(div.className, ' svelte-x1o6ra');
+ assert.equal(div.className, 'svelte-x1o6ra');
component.testName = '';
- assert.equal(div.className, ' svelte-x1o6ra');
+ assert.equal(div.className, 'svelte-x1o6ra');
}
});
diff --git a/packages/svelte/tests/runtime-legacy/samples/await-mutate-array/_config.js b/packages/svelte/tests/runtime-legacy/samples/await-mutate-array/_config.js
index 67ea2cb0a3..70268a0e1d 100644
--- a/packages/svelte/tests/runtime-legacy/samples/await-mutate-array/_config.js
+++ b/packages/svelte/tests/runtime-legacy/samples/await-mutate-array/_config.js
@@ -1,3 +1,4 @@
+import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
@@ -11,7 +12,9 @@ export default test({
const [b1] = target.querySelectorAll('button');
b1.click();
- await Promise.resolve();
+ Promise.resolve();
+ flushSync();
+
assert.htmlEqual(
target.innerHTML,
`2 3 4 \n-------\n1 `
diff --git a/packages/svelte/tests/runtime-legacy/samples/await-then-destruct-object-if/_config.js b/packages/svelte/tests/runtime-legacy/samples/await-then-destruct-object-if/_config.js
index e823c21c9a..af04467749 100644
--- a/packages/svelte/tests/runtime-legacy/samples/await-then-destruct-object-if/_config.js
+++ b/packages/svelte/tests/runtime-legacy/samples/await-then-destruct-object-if/_config.js
@@ -1,3 +1,4 @@
+import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
@@ -11,6 +12,7 @@ export default test({
async test({ assert, component, target }) {
await (component.thePromise = Promise.resolve({ result: 1 }));
+ flushSync();
assert.htmlEqual(
target.innerHTML,
@@ -21,6 +23,7 @@ export default test({
);
await new Promise((resolve) => setTimeout(resolve, 1));
+ flushSync();
assert.htmlEqual(
target.innerHTML,
diff --git a/packages/svelte/tests/runtime-legacy/samples/before-render-chain/_config.js b/packages/svelte/tests/runtime-legacy/samples/before-render-chain/_config.js
index 9364bd9f9e..580df5ca81 100644
--- a/packages/svelte/tests/runtime-legacy/samples/before-render-chain/_config.js
+++ b/packages/svelte/tests/runtime-legacy/samples/before-render-chain/_config.js
@@ -1,3 +1,4 @@
+import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
@@ -9,8 +10,9 @@ export default test({
1
`,
- async test({ assert, component, target }) {
- await component.list.update();
+ test({ assert, component, target }) {
+ component.list.update();
+ flushSync();
assert.htmlEqual(
target.innerHTML,
diff --git a/packages/svelte/tests/runtime-legacy/samples/binding-contenteditable-text-initial/_config.js b/packages/svelte/tests/runtime-legacy/samples/binding-contenteditable-text-initial/_config.js
index e2a28e8dd9..83ad9c2558 100644
--- a/packages/svelte/tests/runtime-legacy/samples/binding-contenteditable-text-initial/_config.js
+++ b/packages/svelte/tests/runtime-legacy/samples/binding-contenteditable-text-initial/_config.js
@@ -1,3 +1,4 @@
+import { flushSync } from 'svelte';
import { ok, test } from '../../test';
export default test({
@@ -18,7 +19,7 @@ export default test({
hello
`,
- async test({ assert, component, target, window }) {
+ test({ assert, component, target, window }) {
assert.equal(component.name, 'world');
const el = target.querySelector('editor');
@@ -27,7 +28,8 @@ export default test({
const event = new window.Event('input');
el.textContent = 'everybody';
- await el.dispatchEvent(event);
+ el.dispatchEvent(event);
+ flushSync();
assert.htmlEqual(
target.innerHTML,
diff --git a/packages/svelte/tests/runtime-legacy/samples/binding-contenteditable-text/_config.js b/packages/svelte/tests/runtime-legacy/samples/binding-contenteditable-text/_config.js
index afecc5b3cd..494c338bfe 100644
--- a/packages/svelte/tests/runtime-legacy/samples/binding-contenteditable-text/_config.js
+++ b/packages/svelte/tests/runtime-legacy/samples/binding-contenteditable-text/_config.js
@@ -1,3 +1,4 @@
+import { flushSync } from 'svelte';
import { ok, test } from '../../test';
export default test({
@@ -10,7 +11,7 @@ export default test({
hello world
`,
- async test({ assert, component, target, window }) {
+ test({ assert, component, target, window }) {
const el = target.querySelector('editor');
ok(el);
assert.equal(el.textContent, 'world');
@@ -18,7 +19,8 @@ export default test({
const event = new window.Event('input');
el.textContent = 'everybody';
- await el.dispatchEvent(event);
+ el.dispatchEvent(event);
+ flushSync();
assert.htmlEqual(
target.innerHTML,
diff --git a/packages/svelte/tests/runtime-legacy/samples/binding-input-checkbox-indeterminate/_config.js b/packages/svelte/tests/runtime-legacy/samples/binding-input-checkbox-indeterminate/_config.js
index 631423feaf..eba77322a1 100644
--- a/packages/svelte/tests/runtime-legacy/samples/binding-input-checkbox-indeterminate/_config.js
+++ b/packages/svelte/tests/runtime-legacy/samples/binding-input-checkbox-indeterminate/_config.js
@@ -1,3 +1,4 @@
+import { flushSync } from 'svelte';
import { ok, test } from '../../test';
export default test({
@@ -17,7 +18,7 @@ export default test({
indeterminate? true
`,
- async test({ assert, component, target, window }) {
+ test({ assert, component, target, window }) {
const input = target.querySelector('input');
ok(input);
@@ -28,7 +29,8 @@ export default test({
input.checked = true;
input.indeterminate = false;
- await input.dispatchEvent(event);
+ input.dispatchEvent(event);
+ flushSync();
assert.equal(component.indeterminate, false);
assert.equal(component.checked, true);
diff --git a/packages/svelte/tests/runtime-legacy/samples/binding-select-in-yield/_config.js b/packages/svelte/tests/runtime-legacy/samples/binding-select-in-yield/_config.js
index f7e5445072..b8f6a9a74a 100644
--- a/packages/svelte/tests/runtime-legacy/samples/binding-select-in-yield/_config.js
+++ b/packages/svelte/tests/runtime-legacy/samples/binding-select-in-yield/_config.js
@@ -1,3 +1,4 @@
+import { flushSync } from 'svelte';
import { ok, test } from '../../test';
export default test({
@@ -7,8 +8,9 @@ export default test({
return { letter: 'b' };
},
- async test({ assert, component, target, window }) {
- await component.modal.toggle();
+ test({ assert, component, target, window }) {
+ component.modal.toggle();
+ flushSync();
assert.htmlEqual(
target.innerHTML,
@@ -28,7 +30,8 @@ export default test({
const change = new window.MouseEvent('change');
select.options[2].selected = true;
- await select.dispatchEvent(change);
+ select.dispatchEvent(change);
+ flushSync();
assert.equal(component.letter, 'c');
assert.deepEqual(
@@ -49,9 +52,9 @@ export default test({
`
);
- await component.modal.toggle();
- await component.modal.toggle();
- await Promise.resolve();
+ component.modal.toggle();
+ component.modal.toggle();
+ flushSync();
select = target.querySelector('select');
ok(select);
diff --git a/packages/svelte/tests/runtime-legacy/samples/bindings-coalesced/_config.js b/packages/svelte/tests/runtime-legacy/samples/bindings-coalesced/_config.js
index 85e1faeb73..3bc5d845a1 100644
--- a/packages/svelte/tests/runtime-legacy/samples/bindings-coalesced/_config.js
+++ b/packages/svelte/tests/runtime-legacy/samples/bindings-coalesced/_config.js
@@ -1,7 +1,8 @@
+import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
- async test({ assert, component }) {
+ test({ assert, component }) {
const { foo, p } = component;
/** @type {string[]} */
@@ -13,7 +14,8 @@ export default test({
}
});
- await foo.double();
+ foo.double();
+ flushSync();
assert.deepEqual(values, ['6']);
}
diff --git a/packages/svelte/tests/runtime-legacy/samples/component-props-mutated/_config.js b/packages/svelte/tests/runtime-legacy/samples/component-props-mutated/_config.js
index 91d0aaa0d3..4a5c102ba2 100644
--- a/packages/svelte/tests/runtime-legacy/samples/component-props-mutated/_config.js
+++ b/packages/svelte/tests/runtime-legacy/samples/component-props-mutated/_config.js
@@ -1,3 +1,4 @@
+import { flushSync } from 'svelte';
import { test } from '../../test';
const data = {
@@ -15,9 +16,10 @@ export default test({
html: 'hello
',
- async test({ assert, component, target }) {
+ test({ assert, component, target }) {
data.message = 'goodbye';
- await component.$set({ data });
+ component.$set({ data });
+ flushSync();
assert.htmlEqual(target.innerHTML, 'goodbye
');
}
diff --git a/packages/svelte/tests/runtime-legacy/samples/component-slot-let-g/_config.js b/packages/svelte/tests/runtime-legacy/samples/component-slot-let-g/_config.js
index db9afae1b0..f3f5e04be1 100644
--- a/packages/svelte/tests/runtime-legacy/samples/component-slot-let-g/_config.js
+++ b/packages/svelte/tests/runtime-legacy/samples/component-slot-let-g/_config.js
@@ -1,3 +1,4 @@
+import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
@@ -5,7 +6,7 @@ export default test({
1
0
`,
- async test({ assert, target, component, window }) {
+ test({ assert, target, component, window }) {
component.x = 2;
assert.htmlEqual(
@@ -17,7 +18,8 @@ export default test({
);
const span = target.querySelector('span');
- await span?.dispatchEvent(new window.MouseEvent('click', { bubbles: true }));
+ span?.dispatchEvent(new window.MouseEvent('click', { bubbles: true }));
+ flushSync();
assert.htmlEqual(
target.innerHTML,
diff --git a/packages/svelte/tests/runtime-legacy/samples/destructured-props-2/_config.js b/packages/svelte/tests/runtime-legacy/samples/destructured-props-2/_config.js
index 950ee4f1ff..0f3fcedce5 100644
--- a/packages/svelte/tests/runtime-legacy/samples/destructured-props-2/_config.js
+++ b/packages/svelte/tests/runtime-legacy/samples/destructured-props-2/_config.js
@@ -1,3 +1,4 @@
+import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
@@ -10,7 +11,7 @@ export default test({
async test({ component, assert, target }) {
await component.update();
- await Promise.resolve();
+ flushSync();
assert.htmlEqual(
target.innerHTML,
diff --git a/packages/svelte/tests/runtime-legacy/samples/destructured-props-3/_config.js b/packages/svelte/tests/runtime-legacy/samples/destructured-props-3/_config.js
index 9ec2810e71..c78b84ec8c 100644
--- a/packages/svelte/tests/runtime-legacy/samples/destructured-props-3/_config.js
+++ b/packages/svelte/tests/runtime-legacy/samples/destructured-props-3/_config.js
@@ -1,3 +1,4 @@
+import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
@@ -8,7 +9,8 @@ export default test({
`,
async test({ component, target, assert }) {
await component.update();
- await Promise.resolve();
+ flushSync();
+
assert.htmlEqual(
target.innerHTML,
`
diff --git a/packages/svelte/tests/runtime-legacy/samples/destructured-props-5/_config.js b/packages/svelte/tests/runtime-legacy/samples/destructured-props-5/_config.js
index 8f7a544bfe..103ce64d7c 100644
--- a/packages/svelte/tests/runtime-legacy/samples/destructured-props-5/_config.js
+++ b/packages/svelte/tests/runtime-legacy/samples/destructured-props-5/_config.js
@@ -1,3 +1,4 @@
+import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
@@ -10,7 +11,7 @@ export default test({
async test({ component, assert, target }) {
await component.update();
- await Promise.resolve();
+ flushSync();
assert.htmlEqual(
target.innerHTML,
diff --git a/packages/svelte/tests/runtime-legacy/samples/destructuring-assignment-array/_config.js b/packages/svelte/tests/runtime-legacy/samples/destructuring-assignment-array/_config.js
index 2df8753114..9c09581097 100644
--- a/packages/svelte/tests/runtime-legacy/samples/destructuring-assignment-array/_config.js
+++ b/packages/svelte/tests/runtime-legacy/samples/destructuring-assignment-array/_config.js
@@ -1,3 +1,4 @@
+import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
@@ -10,9 +11,9 @@ export default test({
`,
- async test({ assert, component, target }) {
- await component.swap(0, 1);
- await Promise.resolve();
+ test({ assert, component, target }) {
+ component.swap(0, 1);
+ flushSync();
assert.htmlEqual(
target.innerHTML,
diff --git a/packages/svelte/tests/runtime-legacy/samples/dynamic-element-animation/_config.js b/packages/svelte/tests/runtime-legacy/samples/dynamic-element-animation/_config.js
index 3d127f1375..05c2dc7304 100644
--- a/packages/svelte/tests/runtime-legacy/samples/dynamic-element-animation/_config.js
+++ b/packages/svelte/tests/runtime-legacy/samples/dynamic-element-animation/_config.js
@@ -50,6 +50,8 @@ export default test({
{ id: 1, name: 'a' }
];
+ raf.tick(0);
+
divs = target.querySelectorAll('div');
assert.equal(divs[0].style.transform, 'translate(0px, 120px)');
assert.equal(divs[1].style.transform, '');
diff --git a/packages/svelte/tests/runtime-legacy/samples/inline-style-directive-spread-and-attr-empty/_config.js b/packages/svelte/tests/runtime-legacy/samples/inline-style-directive-spread-and-attr-empty/_config.js
index 9ff0007c37..04c9868ac3 100644
--- a/packages/svelte/tests/runtime-legacy/samples/inline-style-directive-spread-and-attr-empty/_config.js
+++ b/packages/svelte/tests/runtime-legacy/samples/inline-style-directive-spread-and-attr-empty/_config.js
@@ -2,6 +2,6 @@ import { test } from '../../test';
export default test({
html: `
-
+
`
});
diff --git a/packages/svelte/tests/runtime-legacy/samples/inline-style-optimisation-bailout/_config.js b/packages/svelte/tests/runtime-legacy/samples/inline-style-optimisation-bailout/_config.js
index adcdc4706d..e9965b2b1e 100644
--- a/packages/svelte/tests/runtime-legacy/samples/inline-style-optimisation-bailout/_config.js
+++ b/packages/svelte/tests/runtime-legacy/samples/inline-style-optimisation-bailout/_config.js
@@ -2,7 +2,7 @@ import { ok, test } from '../../test';
export default test({
html: `
- color: red
+ color: red;
`,
test({ assert, component, target, window }) {
diff --git a/packages/svelte/tests/runtime-legacy/samples/inline-style-optimisation-bailout/main.svelte b/packages/svelte/tests/runtime-legacy/samples/inline-style-optimisation-bailout/main.svelte
index 35b768547e..e07adaa1c9 100644
--- a/packages/svelte/tests/runtime-legacy/samples/inline-style-optimisation-bailout/main.svelte
+++ b/packages/svelte/tests/runtime-legacy/samples/inline-style-optimisation-bailout/main.svelte
@@ -1,5 +1,5 @@
{styles}
\ No newline at end of file
diff --git a/packages/svelte/tests/runtime-legacy/samples/key-block-expression-2/_config.js b/packages/svelte/tests/runtime-legacy/samples/key-block-expression-2/_config.js
index a661ec2596..99fa84e2a9 100644
--- a/packages/svelte/tests/runtime-legacy/samples/key-block-expression-2/_config.js
+++ b/packages/svelte/tests/runtime-legacy/samples/key-block-expression-2/_config.js
@@ -1,19 +1,26 @@
+import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
html: '3
',
- async test({ assert, component, target }) {
+ test({ assert, component, target }) {
const div = target.querySelector('div');
- await component.mutate();
+ component.mutate();
+ flushSync();
+
assert.htmlEqual(target.innerHTML, '5
');
assert.strictEqual(div, target.querySelector('div'));
- await component.reassign();
+ component.reassign();
+ flushSync();
+
assert.htmlEqual(target.innerHTML, '7
');
assert.strictEqual(div, target.querySelector('div'));
- await component.changeKey();
+ component.changeKey();
+ flushSync();
+
assert.htmlEqual(target.innerHTML, '7
');
assert.notStrictEqual(div, target.querySelector('div'));
}
diff --git a/packages/svelte/tests/runtime-legacy/samples/lifecycle-render-order-for-children/_config.js b/packages/svelte/tests/runtime-legacy/samples/lifecycle-render-order-for-children/_config.js
index 73bfd09ceb..99f9681c4b 100644
--- a/packages/svelte/tests/runtime-legacy/samples/lifecycle-render-order-for-children/_config.js
+++ b/packages/svelte/tests/runtime-legacy/samples/lifecycle-render-order-for-children/_config.js
@@ -16,13 +16,13 @@ export default test({
test({ assert, compileOptions, component }) {
assert.deepEqual(order, [
'parent: beforeUpdate 0',
- 'parent: render 0',
'1: beforeUpdate 0',
'1: render 0',
'2: beforeUpdate 0',
'2: render 0',
'3: beforeUpdate 0',
'3: render 0',
+ 'parent: render 0',
'1: onMount 0',
'1: afterUpdate 0',
'2: onMount 0',
@@ -39,13 +39,13 @@ export default test({
assert.deepEqual(order, [
'parent: beforeUpdate 1',
- 'parent: render 1',
'1: beforeUpdate 1',
'1: render 1',
'2: beforeUpdate 1',
'2: render 1',
'3: beforeUpdate 1',
'3: render 1',
+ 'parent: render 1',
'1: afterUpdate 1',
'2: afterUpdate 1',
'3: afterUpdate 1',
diff --git a/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access-2/Component.svelte b/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access-2/Component.svelte
new file mode 100644
index 0000000000..73347c4d7f
--- /dev/null
+++ b/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access-2/Component.svelte
@@ -0,0 +1,11 @@
+
+
+{my_prop.foo}
diff --git a/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access-2/_config.js b/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access-2/_config.js
new file mode 100644
index 0000000000..81005cf737
--- /dev/null
+++ b/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access-2/_config.js
@@ -0,0 +1,14 @@
+import { test } from '../../test';
+import { flushSync } from 'svelte';
+
+export default test({
+ async test({ assert, target, logs }) {
+ const [btn1] = target.querySelectorAll('button');
+
+ flushSync(() => {
+ btn1.click();
+ });
+
+ assert.deepEqual(logs, ['bar']);
+ }
+});
diff --git a/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access-2/main.svelte b/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access-2/main.svelte
new file mode 100644
index 0000000000..f38b37fb7f
--- /dev/null
+++ b/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access-2/main.svelte
@@ -0,0 +1,15 @@
+
+
+ {
+ value = undefined;
+ }}>Reset value
+
+{#if value !== undefined}
+
+{/if}
diff --git a/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access-3/Component.svelte b/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access-3/Component.svelte
new file mode 100644
index 0000000000..5bfb777128
--- /dev/null
+++ b/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access-3/Component.svelte
@@ -0,0 +1,5 @@
+
+
+
diff --git a/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access-3/_config.js b/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access-3/_config.js
new file mode 100644
index 0000000000..0eb68310cb
--- /dev/null
+++ b/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access-3/_config.js
@@ -0,0 +1,11 @@
+import { test } from '../../test';
+import { flushSync } from 'svelte';
+
+export default test({
+ async test({ target }) {
+ const [btn1] = target.querySelectorAll('button');
+
+ btn1.click();
+ flushSync();
+ }
+});
diff --git a/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access-3/main.svelte b/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access-3/main.svelte
new file mode 100644
index 0000000000..9c72d2c48a
--- /dev/null
+++ b/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access-3/main.svelte
@@ -0,0 +1,16 @@
+
+
+{#if state}
+ {@const attributes = { title: state.title }}
+
+{/if}
+ {
+ state = undefined;
+ }}
+>
+ Del
+
diff --git a/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access/Component.svelte b/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access/Component.svelte
new file mode 100644
index 0000000000..761f303c2e
--- /dev/null
+++ b/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access/Component.svelte
@@ -0,0 +1,12 @@
+
+
+{count}
+
+ count-- }>
diff --git a/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access/_config.js b/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access/_config.js
new file mode 100644
index 0000000000..2ffb7e653f
--- /dev/null
+++ b/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access/_config.js
@@ -0,0 +1,68 @@
+import { test } from '../../test';
+import { flushSync } from 'svelte';
+
+export default test({
+ async test({ assert, target, logs }) {
+ const [btn1, btn2, btn3] = target.querySelectorAll('button');
+ let ps = [...target.querySelectorAll('p')];
+
+ for (const p of ps) {
+ assert.equal(p.innerHTML, '0');
+ }
+
+ flushSync(() => {
+ btn1.click();
+ });
+
+ // prop update normally if we are not unmounting
+ for (const p of ps) {
+ assert.equal(p.innerHTML, '1');
+ }
+
+ flushSync(() => {
+ btn3.click();
+ });
+
+ // binding still works and update the value correctly
+ for (const p of ps) {
+ assert.equal(p.innerHTML, '0');
+ }
+
+ flushSync(() => {
+ btn1.click();
+ });
+
+ flushSync(() => {
+ btn1.click();
+ });
+
+ console.warn(logs);
+
+ // the five components guarded by `count < 2` unmount and log
+ assert.deepEqual(logs, [1, true, 1, true, 1, true, 1, true, 1, true]);
+
+ flushSync(() => {
+ btn2.click();
+ });
+
+ // the three components guarded by `show` unmount and log
+ assert.deepEqual(logs, [
+ 1,
+ true,
+ 1,
+ true,
+ 1,
+ true,
+ 1,
+ true,
+ 1,
+ true,
+ 2,
+ true,
+ 2,
+ true,
+ 2,
+ true
+ ]);
+ }
+});
diff --git a/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access/main.svelte b/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access/main.svelte
new file mode 100644
index 0000000000..73a7501e9d
--- /dev/null
+++ b/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access/main.svelte
@@ -0,0 +1,41 @@
+
+
+ count++ }>
+ show = !show }>
+
+
+{#if count < 2}
+
+{/if}
+
+
+{#if count < 2}
+
+{/if}
+
+
+{#if count < 2}
+
+{/if}
+
+
+{#if show}
+
+{/if}
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/svelte/tests/runtime-legacy/samples/prop-const/_config.js b/packages/svelte/tests/runtime-legacy/samples/prop-const/_config.js
index 040b911cc6..804ee53342 100644
--- a/packages/svelte/tests/runtime-legacy/samples/prop-const/_config.js
+++ b/packages/svelte/tests/runtime-legacy/samples/prop-const/_config.js
@@ -1,3 +1,4 @@
+import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
@@ -10,11 +11,12 @@ export default test({
b: 2
`,
- async test({ assert, component, target }) {
- await component.$set({
+ test({ assert, component, target }) {
+ component.$set({
a: 5,
b: 6
});
+ flushSync();
assert.htmlEqual(
target.innerHTML,
diff --git a/packages/svelte/tests/runtime-legacy/samples/props-reactive-b/_config.js b/packages/svelte/tests/runtime-legacy/samples/props-reactive-b/_config.js
index b92d81bd53..91133f0bf4 100644
--- a/packages/svelte/tests/runtime-legacy/samples/props-reactive-b/_config.js
+++ b/packages/svelte/tests/runtime-legacy/samples/props-reactive-b/_config.js
@@ -1,3 +1,4 @@
+import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
@@ -11,8 +12,9 @@ export default test({
c: 3
`,
- async test({ assert, component, target }) {
- await component.$set({ a: 4 });
+ test({ assert, component, target }) {
+ component.$set({ a: 4 });
+ flushSync();
assert.htmlEqual(
target.innerHTML,
@@ -23,7 +25,8 @@ export default test({
`
);
- await component.$set({ b: 5 });
+ component.$set({ b: 5 });
+ flushSync();
assert.htmlEqual(
target.innerHTML,
diff --git a/packages/svelte/tests/runtime-legacy/samples/reactive-assignment-in-complex-declaration-with-store-3/_config.js b/packages/svelte/tests/runtime-legacy/samples/reactive-assignment-in-complex-declaration-with-store-3/_config.js
index 2325d17cd5..1a67e46b38 100644
--- a/packages/svelte/tests/runtime-legacy/samples/reactive-assignment-in-complex-declaration-with-store-3/_config.js
+++ b/packages/svelte/tests/runtime-legacy/samples/reactive-assignment-in-complex-declaration-with-store-3/_config.js
@@ -1,3 +1,4 @@
+import { flushSync } from 'svelte';
import { test } from '../../test';
import { store } from './store.js';
@@ -6,10 +7,9 @@ export default test({
before_test() {
store.reset();
},
- async test({ assert, target }) {
+ test({ assert, target }) {
store.set(42);
-
- await Promise.resolve();
+ flushSync();
assert.htmlEqual(target.innerHTML, '42 ');
diff --git a/packages/svelte/tests/runtime-legacy/samples/reactive-value-dependency-not-referenced/_config.js b/packages/svelte/tests/runtime-legacy/samples/reactive-value-dependency-not-referenced/_config.js
index f2f7fe92b1..3b31ee766e 100644
--- a/packages/svelte/tests/runtime-legacy/samples/reactive-value-dependency-not-referenced/_config.js
+++ b/packages/svelte/tests/runtime-legacy/samples/reactive-value-dependency-not-referenced/_config.js
@@ -1,3 +1,4 @@
+import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
@@ -6,29 +7,35 @@ export default test({
42
`,
- async test({ assert, component, target }) {
- await component.updateStore(undefined);
- await Promise.resolve();
+ test({ assert, component, target }) {
+ component.updateStore(undefined);
+ flushSync();
+
assert.htmlEqual(target.innerHTML, '
42
');
- await component.updateStore(33);
- await Promise.resolve();
+ component.updateStore(33);
+ flushSync();
+
assert.htmlEqual(target.innerHTML, '33
42
');
- await component.updateStore(undefined);
- await Promise.resolve();
+ component.updateStore(undefined);
+ flushSync();
+
assert.htmlEqual(target.innerHTML, '
42
');
- await component.updateVar(undefined);
- await Promise.resolve();
+ component.updateVar(undefined);
+ flushSync();
+
assert.htmlEqual(target.innerHTML, '
');
- await component.updateVar(33);
- await Promise.resolve();
+ component.updateVar(33);
+ flushSync();
+
assert.htmlEqual(target.innerHTML, '
33
');
- await component.updateVar(undefined);
- await Promise.resolve();
+ component.updateVar(undefined);
+ flushSync();
+
assert.htmlEqual(target.innerHTML, '
');
}
});
diff --git a/packages/svelte/tests/runtime-legacy/samples/reactive-value-function/_config.js b/packages/svelte/tests/runtime-legacy/samples/reactive-value-function/_config.js
index cbcb19d95e..5317ab496f 100644
--- a/packages/svelte/tests/runtime-legacy/samples/reactive-value-function/_config.js
+++ b/packages/svelte/tests/runtime-legacy/samples/reactive-value-function/_config.js
@@ -1,10 +1,12 @@
+import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
html: '1-2',
- async test({ assert, component, target }) {
- await component.update();
+ test({ assert, component, target }) {
+ component.update();
+ flushSync();
assert.htmlEqual(target.innerHTML, '3-4');
}
diff --git a/packages/svelte/tests/runtime-legacy/samples/reactive-values-text-node/_config.js b/packages/svelte/tests/runtime-legacy/samples/reactive-values-text-node/_config.js
index e05c8c80dd..e97a046f68 100644
--- a/packages/svelte/tests/runtime-legacy/samples/reactive-values-text-node/_config.js
+++ b/packages/svelte/tests/runtime-legacy/samples/reactive-values-text-node/_config.js
@@ -1,5 +1,6 @@
import { test } from '../../test';
import { create_deferred } from '../../../helpers';
+import { flushSync } from 'svelte';
/** @type {ReturnType} */
let deferred;
@@ -17,6 +18,8 @@ export default test({
async test({ assert, target }) {
await deferred.promise;
+ flushSync();
+
assert.htmlEqual(
target.innerHTML,
`
diff --git a/packages/svelte/tests/runtime-legacy/samples/spread-own-props/_config.js b/packages/svelte/tests/runtime-legacy/samples/spread-own-props/_config.js
index 708e3615f1..7486055440 100644
--- a/packages/svelte/tests/runtime-legacy/samples/spread-own-props/_config.js
+++ b/packages/svelte/tests/runtime-legacy/samples/spread-own-props/_config.js
@@ -1,3 +1,4 @@
+import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
@@ -17,13 +18,14 @@ export default test({
quux: core
`,
- async test({ assert, component, target }) {
- await component.$set({
+ test({ assert, component, target }) {
+ component.$set({
foo: 'wut',
baz: 40 + 3,
qux: `this is a ${'rather boring'} string`,
quux: 'heart'
});
+ flushSync();
assert.htmlEqual(
target.innerHTML,
diff --git a/packages/svelte/tests/runtime-legacy/samples/store-auto-subscribe-in-reactive-declaration-2/_config.js b/packages/svelte/tests/runtime-legacy/samples/store-auto-subscribe-in-reactive-declaration-2/_config.js
index 8e0886c668..d380150e55 100644
--- a/packages/svelte/tests/runtime-legacy/samples/store-auto-subscribe-in-reactive-declaration-2/_config.js
+++ b/packages/svelte/tests/runtime-legacy/samples/store-auto-subscribe-in-reactive-declaration-2/_config.js
@@ -1,3 +1,4 @@
+import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
@@ -6,10 +7,9 @@ export default test({
Hello World
`,
- async test({ assert, component, target }) {
- await component.update_value('Hi Svelte');
- await Promise.resolve();
- await Promise.resolve();
+ test({ assert, component, target }) {
+ component.update_value('Hi Svelte');
+ flushSync();
assert.htmlEqual(
target.innerHTML,
diff --git a/packages/svelte/tests/runtime-legacy/samples/store-increment-updates-reactive/_config.js b/packages/svelte/tests/runtime-legacy/samples/store-increment-updates-reactive/_config.js
index 1c81f7c4e8..89ff695d9d 100644
--- a/packages/svelte/tests/runtime-legacy/samples/store-increment-updates-reactive/_config.js
+++ b/packages/svelte/tests/runtime-legacy/samples/store-increment-updates-reactive/_config.js
@@ -1,10 +1,13 @@
+import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
html: '0',
- async test({ assert, component, target }) {
- await component.increment();
+ test({ assert, component, target }) {
+ component.increment();
+ flushSync();
+
assert.htmlEqual(target.innerHTML, '1');
}
});
diff --git a/packages/svelte/tests/runtime-legacy/samples/store-unreferenced/_config.js b/packages/svelte/tests/runtime-legacy/samples/store-unreferenced/_config.js
index 808ed899d3..7096f5d1fe 100644
--- a/packages/svelte/tests/runtime-legacy/samples/store-unreferenced/_config.js
+++ b/packages/svelte/tests/runtime-legacy/samples/store-unreferenced/_config.js
@@ -1,3 +1,4 @@
+import { flushSync } from 'svelte';
import { test } from '../../test';
import { count } from './store.js';
@@ -8,8 +9,9 @@ export default test({
count.set(0);
},
- async test({ assert, component, target }) {
- await component.increment();
+ test({ assert, component, target }) {
+ component.increment();
+ flushSync();
assert.htmlEqual(target.innerHTML, 'count: 1
');
}
diff --git a/packages/svelte/tests/runtime-legacy/samples/window-event-custom/_config.js b/packages/svelte/tests/runtime-legacy/samples/window-event-custom/_config.js
index 32b1c65154..f6b7a8af42 100644
--- a/packages/svelte/tests/runtime-legacy/samples/window-event-custom/_config.js
+++ b/packages/svelte/tests/runtime-legacy/samples/window-event-custom/_config.js
@@ -1,14 +1,16 @@
+import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
html: 'escaped: false
',
- async test({ assert, target, window }) {
+ test({ assert, target, window }) {
const event = new window.KeyboardEvent('keydown', {
key: 'Escape'
});
- await window.dispatchEvent(event);
+ window.dispatchEvent(event);
+ flushSync();
assert.htmlEqual(
target.innerHTML,
diff --git a/packages/svelte/tests/runtime-legacy/shared.ts b/packages/svelte/tests/runtime-legacy/shared.ts
index e6dc0f385b..fc748ce6b2 100644
--- a/packages/svelte/tests/runtime-legacy/shared.ts
+++ b/packages/svelte/tests/runtime-legacy/shared.ts
@@ -1,6 +1,6 @@
import * as fs from 'node:fs';
import { setImmediate } from 'node:timers/promises';
-import glob from 'tiny-glob/sync.js';
+import { globSync } from 'tinyglobby';
import { createClassComponent } from 'svelte/legacy';
import { proxy } from 'svelte/internal/client';
import { flushSync, hydrate, mount, unmount } from 'svelte';
@@ -36,6 +36,7 @@ export interface RuntimeTest = Record;
props?: Props;
server_props?: Props;
+ id_prefix?: string;
before_test?: () => void;
after_test?: () => void;
test?: (args: {
@@ -256,7 +257,7 @@ async function run_test_variant(
raf.reset();
// Put things we need on window for testing
- const styles = glob('**/*.css', { cwd: `${cwd}/_output/client` })
+ const styles = globSync('**/*.css', { cwd: `${cwd}/_output/client` })
.map((file) => fs.readFileSync(`${cwd}/_output/client/${file}`, 'utf-8'))
.join('\n')
.replace(/\/\*<\/?style>\*\//g, '');
@@ -284,7 +285,8 @@ async function run_test_variant(
// ssr into target
const SsrSvelteComponent = (await import(`${cwd}/_output/server/main.svelte.js`)).default;
const { html, head } = render(SsrSvelteComponent, {
- props: config.server_props ?? config.props ?? {}
+ props: config.server_props ?? config.props ?? {},
+ idPrefix: config.id_prefix
});
fs.writeFileSync(`${cwd}/_output/rendered.html`, html);
@@ -345,6 +347,10 @@ async function run_test_variant(
if (runes) {
props = proxy({ ...(config.props || {}) });
+
+ // @ts-expect-error
+ globalThis.__svelte.uid = 1;
+
if (manual_hydrate) {
hydrate_fn = () => {
instance = hydrate(mod.default, {
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/autofocus-with-call/_config.js b/packages/svelte/tests/runtime-runes/samples/autofocus-with-call/_config.js
new file mode 100644
index 0000000000..0597c2fda8
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/autofocus-with-call/_config.js
@@ -0,0 +1,7 @@
+import { test } from '../../test';
+
+export default test({
+ async test({ assert, errors }) {
+ assert.deepEqual(errors, []);
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/autofocus-with-call/main.svelte b/packages/svelte/tests/runtime-runes/samples/autofocus-with-call/main.svelte
new file mode 100644
index 0000000000..cb3804af34
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/autofocus-with-call/main.svelte
@@ -0,0 +1,6 @@
+
+
+
+
\ No newline at end of file
diff --git a/packages/svelte/tests/runtime-runes/samples/await-hydrate-maybe-promise/_config.js b/packages/svelte/tests/runtime-runes/samples/await-hydrate-maybe-promise/_config.js
new file mode 100644
index 0000000000..f81b41d41a
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/await-hydrate-maybe-promise/_config.js
@@ -0,0 +1,23 @@
+import { flushSync } from 'svelte';
+import { test } from '../../test';
+
+export default test({
+ ssrHtml: 'fulfil 42
loading...
',
+ html: 'fulfil loading...
42
',
+
+ props: {
+ browser: true
+ },
+
+ server_props: {
+ browser: false
+ },
+
+ async test({ assert, target }) {
+ const button = target.querySelector('button');
+
+ flushSync(() => button?.click());
+ await Promise.resolve();
+ assert.htmlEqual(target.innerHTML, 'fulfil 42
42
');
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/await-hydrate-maybe-promise/main.svelte b/packages/svelte/tests/runtime-runes/samples/await-hydrate-maybe-promise/main.svelte
new file mode 100644
index 0000000000..d8d0cd4027
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/await-hydrate-maybe-promise/main.svelte
@@ -0,0 +1,25 @@
+
+
+ fulfil(42)}>fulfil
+
+{#await a}
+ {#if true}loading...
{/if}
+{:then a}
+ {a}
+{/await}
+
+
+
+{#await b}
+ {#if true}loading...
{/if}
+{:then b}
+ {b}
+{/await}
diff --git a/packages/svelte/tests/runtime-runes/samples/bind-current-time-remove-listener/_config.js b/packages/svelte/tests/runtime-runes/samples/bind-current-time-remove-listener/_config.js
new file mode 100644
index 0000000000..29dc5e8d7f
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/bind-current-time-remove-listener/_config.js
@@ -0,0 +1,26 @@
+import { flushSync } from 'svelte';
+import { ok, test } from '../../test';
+
+export default test({
+ async test({ assert, target, logs }) {
+ const audio = target.querySelector('audio');
+ const btn = target.querySelector('button');
+
+ ok(audio);
+
+ flushSync(() => {
+ audio.currentTime = 10;
+ audio.dispatchEvent(new Event('timeupdate'));
+ });
+ assert.deepEqual(logs, ['event']);
+
+ flushSync(() => {
+ btn?.click();
+ });
+ flushSync(() => {
+ audio.currentTime = 20;
+ audio.dispatchEvent(new Event('timeupdate'));
+ });
+ assert.deepEqual(logs, ['event']);
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/bind-current-time-remove-listener/main.svelte b/packages/svelte/tests/runtime-runes/samples/bind-current-time-remove-listener/main.svelte
new file mode 100644
index 0000000000..40c215378e
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/bind-current-time-remove-listener/main.svelte
@@ -0,0 +1,12 @@
+
+
+ show = false}>
+{#if show}
+ time,(new_time)=>{
+ console.log("event");
+ time = new_time;
+ }}>
+{/if}
\ No newline at end of file
diff --git a/packages/svelte/tests/runtime-runes/samples/class-directive-mutations/_config.js b/packages/svelte/tests/runtime-runes/samples/class-directive-mutations/_config.js
new file mode 100644
index 0000000000..076efee994
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/class-directive-mutations/_config.js
@@ -0,0 +1,126 @@
+import { flushSync, tick } from 'svelte';
+import { test } from '../../test';
+
+// This test counts mutations on hydration
+// set_class() should not mutate class on hydration, except if mismatch
+export default test({
+ mode: ['server', 'hydrate'],
+
+ server_props: {
+ browser: false
+ },
+
+ props: {
+ browser: true
+ },
+
+ html: `
+
+
+
+
+
+
+
+
+
+
+
+ `,
+
+ ssrHtml: `
+
+
+
+
+
+
+
+
+
+
+
+ `,
+
+ async test({ target, assert, component, instance }) {
+ flushSync();
+ tick();
+ assert.deepEqual(instance.get_and_clear_mutations(), ['MAIN']);
+
+ component.foo = false;
+ flushSync();
+ tick();
+ assert.deepEqual(
+ instance.get_and_clear_mutations(),
+ ['DIV', 'SPAN', 'B', 'I', 'DIV', 'SPAN', 'B', 'I'],
+ 'first mutation'
+ );
+
+ assert.htmlEqual(
+ target.innerHTML,
+ `
+
+
+
+
+
+
+
+
+
+
+
+ `
+ );
+
+ component.foo = true;
+ flushSync();
+ assert.deepEqual(
+ instance.get_and_clear_mutations(),
+ ['DIV', 'SPAN', 'B', 'I', 'DIV', 'SPAN', 'B', 'I'],
+ 'second mutation'
+ );
+
+ assert.htmlEqual(
+ target.innerHTML,
+ `
+
+
+
+
+
+
+
+
+
+
+
+ `
+ );
+
+ component.classname = 'another';
+ flushSync();
+ assert.deepEqual(
+ instance.get_and_clear_mutations(),
+ ['DIV', 'B', 'DIV', 'B'],
+ 'class mutation'
+ );
+
+ assert.htmlEqual(
+ target.innerHTML,
+ `
+
+
+
+
+
+
+
+
+
+
+
+ `
+ );
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/class-directive-mutations/main.svelte b/packages/svelte/tests/runtime-runes/samples/class-directive-mutations/main.svelte
new file mode 100644
index 0000000000..d748988e21
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/class-directive-mutations/main.svelte
@@ -0,0 +1,52 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/svelte/tests/runtime-runes/samples/class-directive/_config.js b/packages/svelte/tests/runtime-runes/samples/class-directive/_config.js
new file mode 100644
index 0000000000..2756b40493
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/class-directive/_config.js
@@ -0,0 +1,145 @@
+import { flushSync } from 'svelte';
+import { test } from '../../test';
+
+export default test({
+ html: `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `,
+ test({ assert, target, component }) {
+ component.foo = true;
+ flushSync();
+
+ assert.htmlEqual(
+ target.innerHTML,
+ `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `
+ );
+
+ component.bar = false;
+ flushSync();
+
+ assert.htmlEqual(
+ target.innerHTML,
+ `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `
+ );
+
+ component.foo = false;
+ flushSync();
+
+ assert.htmlEqual(
+ target.innerHTML,
+ `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `
+ );
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/class-directive/main.svelte b/packages/svelte/tests/runtime-runes/samples/class-directive/main.svelte
new file mode 100644
index 0000000000..966c07a78e
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/class-directive/main.svelte
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/svelte/tests/runtime-runes/samples/const-tag-boundary/FlakyComponent.svelte b/packages/svelte/tests/runtime-runes/samples/const-tag-boundary/FlakyComponent.svelte
new file mode 100644
index 0000000000..8bbec90de4
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/const-tag-boundary/FlakyComponent.svelte
@@ -0,0 +1,3 @@
+
\ No newline at end of file
diff --git a/packages/svelte/tests/runtime-runes/samples/const-tag-boundary/_config.js b/packages/svelte/tests/runtime-runes/samples/const-tag-boundary/_config.js
new file mode 100644
index 0000000000..4338969a48
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/const-tag-boundary/_config.js
@@ -0,0 +1,17 @@
+import { flushSync } from 'svelte';
+import { test } from '../../test';
+
+export default test({
+ html: '2
',
+ mode: ['client'],
+ test({ target, assert }) {
+ const btn = target.querySelector('button');
+ const p = target.querySelector('p');
+
+ flushSync(() => {
+ btn?.click();
+ });
+
+ assert.equal(p?.innerHTML, '4');
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/const-tag-boundary/main.svelte b/packages/svelte/tests/runtime-runes/samples/const-tag-boundary/main.svelte
new file mode 100644
index 0000000000..25ea8a3ffc
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/const-tag-boundary/main.svelte
@@ -0,0 +1,14 @@
+
+
+test++}>
+
+
+ {@const double = test * 2}
+ {#snippet failed()}
+ {double}
+ {/snippet}
+
+
\ No newline at end of file
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/derived-unowned-2/_config.js b/packages/svelte/tests/runtime-runes/samples/derived-unowned-2/_config.js
index 3604000543..3ca98bb0c6 100644
--- a/packages/svelte/tests/runtime-runes/samples/derived-unowned-2/_config.js
+++ b/packages/svelte/tests/runtime-runes/samples/derived-unowned-2/_config.js
@@ -1,3 +1,4 @@
+import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
@@ -7,7 +8,7 @@ export default test({
async test({ assert, target }) {
await Promise.resolve();
- await Promise.resolve();
+ flushSync();
assert.htmlEqual(
target.innerHTML,
'd2: 3,4,5
d3: 3,4,5
d4: 3,4,5
'
diff --git a/packages/svelte/tests/runtime-runes/samples/derived-unowned-5/_config.js b/packages/svelte/tests/runtime-runes/samples/derived-unowned-5/_config.js
index 20c3cc112e..5f38394d06 100644
--- a/packages/svelte/tests/runtime-runes/samples/derived-unowned-5/_config.js
+++ b/packages/svelte/tests/runtime-runes/samples/derived-unowned-5/_config.js
@@ -1,3 +1,4 @@
+import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
@@ -5,7 +6,7 @@ export default test({
// The test has a bunch of queueMicrotasks
await Promise.resolve();
await Promise.resolve();
- await Promise.resolve();
+ flushSync();
assert.htmlEqual(target.innerHTML, `Zeeba Neighba
`);
}
diff --git a/packages/svelte/tests/runtime-runes/samples/deriveds-in-constructor/_config.js b/packages/svelte/tests/runtime-runes/samples/deriveds-in-constructor/_config.js
new file mode 100644
index 0000000000..b364a989f4
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/deriveds-in-constructor/_config.js
@@ -0,0 +1,5 @@
+import { test } from '../../test';
+
+export default test({
+ html: `state,derived state,derived.by derived state
`
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/deriveds-in-constructor/main.svelte b/packages/svelte/tests/runtime-runes/samples/deriveds-in-constructor/main.svelte
new file mode 100644
index 0000000000..bc8efba7e7
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/deriveds-in-constructor/main.svelte
@@ -0,0 +1,18 @@
+
+
+{foo.initial}
\ No newline at end of file
diff --git a/packages/svelte/tests/runtime-runes/samples/dynamic-style-attr/_config.js b/packages/svelte/tests/runtime-runes/samples/dynamic-style-attr/_config.js
index f682972179..20092ddadf 100644
--- a/packages/svelte/tests/runtime-runes/samples/dynamic-style-attr/_config.js
+++ b/packages/svelte/tests/runtime-runes/samples/dynamic-style-attr/_config.js
@@ -2,7 +2,7 @@ import { test } from '../../test';
import { flushSync } from 'svelte';
export default test({
- html: `Hello world
Make blue Hello worldMake blue Hello worldMake blue Hello worldMake blue color;
-Hello world
+Hello world
color = 'blue'}>Make blue
diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-global-2/_config.js b/packages/svelte/tests/runtime-runes/samples/each-bind-store-no-warning/_config.js
similarity index 99%
rename from packages/svelte/tests/runtime-runes/samples/non-local-mutation-global-2/_config.js
rename to packages/svelte/tests/runtime-runes/samples/each-bind-store-no-warning/_config.js
index b4864154c3..dba2e85650 100644
--- a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-global-2/_config.js
+++ b/packages/svelte/tests/runtime-runes/samples/each-bind-store-no-warning/_config.js
@@ -4,7 +4,6 @@ export default test({
compileOptions: {
dev: true
},
-
async test({ assert, warnings }) {
assert.deepEqual(warnings, []);
}
diff --git a/packages/svelte/tests/runtime-runes/samples/each-bind-store-no-warning/main.svelte b/packages/svelte/tests/runtime-runes/samples/each-bind-store-no-warning/main.svelte
new file mode 100644
index 0000000000..f927bf079a
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/each-bind-store-no-warning/main.svelte
@@ -0,0 +1,10 @@
+
+
+
+{#each $array as item}
+
+{/each}
diff --git a/packages/svelte/tests/runtime-runes/samples/effect-cleanup/_config.js b/packages/svelte/tests/runtime-runes/samples/effect-cleanup/_config.js
index 6a3d9eef77..e55733c148 100644
--- a/packages/svelte/tests/runtime-runes/samples/effect-cleanup/_config.js
+++ b/packages/svelte/tests/runtime-runes/samples/effect-cleanup/_config.js
@@ -10,6 +10,6 @@ export default test({
flushSync(() => {
b1.click();
});
- assert.deepEqual(logs, ['init 0', 'cleanup 2', null, 'init 2', 'cleanup 4', null, 'init 4']);
+ assert.deepEqual(logs, ['init 0']);
}
});
diff --git a/packages/svelte/tests/runtime-runes/samples/effect-root-5/_config.js b/packages/svelte/tests/runtime-runes/samples/effect-root-5/_config.js
new file mode 100644
index 0000000000..260c757e3d
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/effect-root-5/_config.js
@@ -0,0 +1,17 @@
+import { flushSync } from 'svelte';
+import { test } from '../../test';
+
+export default test({
+ async test({ assert, target, logs }) {
+ const [b1, b2] = target.querySelectorAll('button');
+
+ flushSync(() => b1.click());
+ assert.deepEqual(logs, [0, 1]);
+
+ flushSync(() => b1.click());
+ assert.deepEqual(logs, [0, 1, 2]);
+
+ flushSync(() => b2.click());
+ assert.deepEqual(logs, [0, 1, 2]);
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/effect-root-5/main.svelte b/packages/svelte/tests/runtime-runes/samples/effect-root-5/main.svelte
new file mode 100644
index 0000000000..06655a5362
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/effect-root-5/main.svelte
@@ -0,0 +1,23 @@
+
+
+ ((obj ??= { count: 0 }).count += 1)}>+1
+ (obj = null)}>null
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/error-boundary-21/Child.svelte b/packages/svelte/tests/runtime-runes/samples/error-boundary-21/Child.svelte
new file mode 100644
index 0000000000..ea60542af9
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/error-boundary-21/Child.svelte
@@ -0,0 +1,3 @@
+
diff --git a/packages/svelte/tests/runtime-runes/samples/error-boundary-21/_config.js b/packages/svelte/tests/runtime-runes/samples/error-boundary-21/_config.js
new file mode 100644
index 0000000000..e301f83e60
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/error-boundary-21/_config.js
@@ -0,0 +1,17 @@
+import { flushSync } from 'svelte';
+import { test } from '../../test';
+
+export default test({
+ html: '0
',
+ mode: ['client'],
+ test({ assert, target }) {
+ let btn = target.querySelector('button');
+ let div = target.querySelector('div');
+
+ flushSync(() => {
+ btn?.click();
+ });
+
+ assert.equal(div?.innerHTML, `1`);
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/error-boundary-21/main.svelte b/packages/svelte/tests/runtime-runes/samples/error-boundary-21/main.svelte
new file mode 100644
index 0000000000..ed3140b1ef
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/error-boundary-21/main.svelte
@@ -0,0 +1,14 @@
+
+
+count++}>
+
+
+
+ {#snippet failed()}
+ {count}
+ {/snippet}
+
diff --git a/packages/svelte/tests/runtime-runes/samples/event-handler-invalid-values/_config.js b/packages/svelte/tests/runtime-runes/samples/event-handler-invalid-values/_config.js
new file mode 100644
index 0000000000..d53812d4c3
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/event-handler-invalid-values/_config.js
@@ -0,0 +1,48 @@
+import { assertType } from 'vitest';
+import { test } from '../../test';
+
+export default test({
+ mode: ['client'],
+
+ compileOptions: {
+ dev: true
+ },
+
+ test({ assert, target, warnings, logs }) {
+ /** @type {any} */
+ let error = null;
+
+ const handler = (/** @type {any} */ e) => {
+ error = e.error;
+ e.stopImmediatePropagation();
+ };
+
+ window.addEventListener('error', handler, true);
+
+ const [b1, b2, b3] = target.querySelectorAll('button');
+
+ b1.click();
+ assert.deepEqual(logs, []);
+ assert.equal(error, null);
+
+ error = null;
+ logs.length = 0;
+
+ b2.click();
+ assert.deepEqual(logs, ['clicked']);
+ assert.equal(error, null);
+
+ error = null;
+ logs.length = 0;
+
+ b3.click();
+ assert.deepEqual(logs, []);
+ assert.deepEqual(warnings, [
+ '`click` handler at main.svelte:10:17 should be a function. Did you mean to add a leading `() =>`?'
+ ]);
+ assert.isNotNull(error);
+ assert.match(error.message, /is not a function/);
+
+ window.removeEventListener('error', handler, true);
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/event-handler-invalid-values/main.svelte b/packages/svelte/tests/runtime-runes/samples/event-handler-invalid-values/main.svelte
new file mode 100644
index 0000000000..f6e344ece8
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/event-handler-invalid-values/main.svelte
@@ -0,0 +1,10 @@
+
+
+click
+click
+click
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/head-payload-validation/_config.js b/packages/svelte/tests/runtime-runes/samples/head-payload-validation/_config.js
new file mode 100644
index 0000000000..7c609205df
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/head-payload-validation/_config.js
@@ -0,0 +1,11 @@
+import { test } from '../../test';
+
+export default test({
+ compileOptions: {
+ dev: true
+ },
+ mode: ['server'],
+ async test({ errors, assert }) {
+ assert.equal(errors, []);
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/head-payload-validation/main.svelte b/packages/svelte/tests/runtime-runes/samples/head-payload-validation/main.svelte
new file mode 100644
index 0000000000..7eb31d3a9e
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/head-payload-validation/main.svelte
@@ -0,0 +1,7 @@
+{#snippet head()}
+ Cool
+{/snippet}
+
+
+ {@render head()}
+
\ No newline at end of file
diff --git a/packages/svelte/tests/runtime-runes/samples/lifecycle-render-order-for-children-2/_config.js b/packages/svelte/tests/runtime-runes/samples/lifecycle-render-order-for-children-2/_config.js
index 1066d9a2df..cb8e648645 100644
--- a/packages/svelte/tests/runtime-runes/samples/lifecycle-render-order-for-children-2/_config.js
+++ b/packages/svelte/tests/runtime-runes/samples/lifecycle-render-order-for-children-2/_config.js
@@ -10,7 +10,6 @@ export default test({
assert.deepEqual(logs, [
'parent: $effect.pre 0',
'parent: $effect.pre (2) 0',
- 'parent: render 0',
'1: $effect.pre 0',
'1: $effect.pre (2) 0',
'1: render 0',
@@ -20,6 +19,7 @@ export default test({
'3: $effect.pre 0',
'3: $effect.pre (2) 0',
'3: render 0',
+ 'parent: render 0',
'1: $effect 0',
'2: $effect 0',
'3: $effect 0',
@@ -33,7 +33,6 @@ export default test({
assert.deepEqual(logs, [
'parent: $effect.pre 1',
'parent: $effect.pre (2) 1',
- 'parent: render 1',
'1: $effect.pre 1',
'1: $effect.pre (2) 1',
'1: render 1',
@@ -43,6 +42,7 @@ export default test({
'3: $effect.pre 1',
'3: $effect.pre (2) 1',
'3: render 1',
+ 'parent: render 1',
'1: $effect 1',
'2: $effect 1',
'3: $effect 1',
diff --git a/packages/svelte/tests/runtime-runes/samples/lifecycle-render-order-for-children-3/_config.js b/packages/svelte/tests/runtime-runes/samples/lifecycle-render-order-for-children-3/_config.js
index 55847c35a2..6c063bcb3e 100644
--- a/packages/svelte/tests/runtime-runes/samples/lifecycle-render-order-for-children-3/_config.js
+++ b/packages/svelte/tests/runtime-runes/samples/lifecycle-render-order-for-children-3/_config.js
@@ -8,13 +8,13 @@ export default test({
async test({ assert, component, logs }) {
assert.deepEqual(logs, [
- 'parent: render 0',
'1: $effect.pre 0',
'1: render 0',
'2: $effect.pre 0',
'2: render 0',
'3: $effect.pre 0',
'3: render 0',
+ 'parent: render 0',
'1: $effect 0',
'2: $effect 0',
'3: $effect 0',
@@ -26,13 +26,13 @@ export default test({
flushSync(() => (component.n += 1));
assert.deepEqual(logs, [
- 'parent: render 1',
'1: $effect.pre 1',
'1: render 1',
'2: $effect.pre 1',
'2: render 1',
'3: $effect.pre 1',
'3: render 1',
+ 'parent: render 1',
'1: $effect 1',
'2: $effect 1',
'3: $effect 1',
diff --git a/packages/svelte/tests/runtime-runes/samples/lifecycle-render-order-for-children-4/_config.js b/packages/svelte/tests/runtime-runes/samples/lifecycle-render-order-for-children-4/_config.js
index 0cd7e15a37..29b0b67a52 100644
--- a/packages/svelte/tests/runtime-runes/samples/lifecycle-render-order-for-children-4/_config.js
+++ b/packages/svelte/tests/runtime-runes/samples/lifecycle-render-order-for-children-4/_config.js
@@ -10,7 +10,6 @@ export default test({
assert.deepEqual(logs, [
'parent: $effect.pre 0',
'parent: nested $effect.pre 0',
- 'parent: render 0',
'1: $effect.pre 0',
'1: nested $effect.pre 0',
'1: render 0',
@@ -20,6 +19,7 @@ export default test({
'3: $effect.pre 0',
'3: nested $effect.pre 0',
'3: render 0',
+ 'parent: render 0',
'1: $effect 0',
'2: $effect 0',
'3: $effect 0',
@@ -33,7 +33,6 @@ export default test({
assert.deepEqual(logs, [
'parent: $effect.pre 1',
'parent: nested $effect.pre 1',
- 'parent: render 1',
'1: $effect.pre 1',
'1: nested $effect.pre 1',
'1: render 1',
@@ -43,6 +42,7 @@ export default test({
'3: $effect.pre 1',
'3: nested $effect.pre 1',
'3: render 1',
+ 'parent: render 1',
'1: $effect 1',
'2: $effect 1',
'3: $effect 1',
diff --git a/packages/svelte/tests/runtime-runes/samples/lifecycle-render-order-for-children/_config.js b/packages/svelte/tests/runtime-runes/samples/lifecycle-render-order-for-children/_config.js
index 19b8fb3938..3138ec7231 100644
--- a/packages/svelte/tests/runtime-runes/samples/lifecycle-render-order-for-children/_config.js
+++ b/packages/svelte/tests/runtime-runes/samples/lifecycle-render-order-for-children/_config.js
@@ -9,13 +9,13 @@ export default test({
async test({ assert, component, logs }) {
assert.deepEqual(logs, [
'parent: $effect.pre 0',
- 'parent: render 0',
'1: $effect.pre 0',
'1: render 0',
'2: $effect.pre 0',
'2: render 0',
'3: $effect.pre 0',
'3: render 0',
+ 'parent: render 0',
'1: $effect 0',
'2: $effect 0',
'3: $effect 0',
@@ -28,13 +28,13 @@ export default test({
assert.deepEqual(logs, [
'parent: $effect.pre 1',
- 'parent: render 1',
'1: $effect.pre 1',
'1: render 1',
'2: $effect.pre 1',
'2: render 1',
'3: $effect.pre 1',
'3: render 1',
+ 'parent: render 1',
'1: $effect 1',
'2: $effect 1',
'3: $effect 1',
diff --git a/packages/svelte/tests/runtime-runes/samples/muted-without-bind-works/_config.js b/packages/svelte/tests/runtime-runes/samples/muted-without-bind-works/_config.js
new file mode 100644
index 0000000000..cc4dfb37f0
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/muted-without-bind-works/_config.js
@@ -0,0 +1,13 @@
+import { flushSync } from 'svelte';
+import { ok, test } from '../../test';
+
+export default test({
+ async test({ assert, target, logs }) {
+ const btn = target.querySelector('button');
+ ok(btn);
+ flushSync(() => {
+ btn.click();
+ });
+ assert.deepEqual(logs, [true]);
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/muted-without-bind-works/main.svelte b/packages/svelte/tests/runtime-runes/samples/muted-without-bind-works/main.svelte
new file mode 100644
index 0000000000..646334c1ec
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/muted-without-bind-works/main.svelte
@@ -0,0 +1,13 @@
+
+
+
+ (muted = !muted)}>
+
diff --git a/packages/svelte/tests/runtime-runes/samples/nested-effect-conflict/_config.js b/packages/svelte/tests/runtime-runes/samples/nested-effect-conflict/_config.js
index a8c16b7008..eb631bc9f4 100644
--- a/packages/svelte/tests/runtime-runes/samples/nested-effect-conflict/_config.js
+++ b/packages/svelte/tests/runtime-runes/samples/nested-effect-conflict/_config.js
@@ -10,14 +10,6 @@ export default test({
});
await Promise.resolve();
- assert.deepEqual(logs, [
- 'top level',
- 'inner',
- 0,
- 'destroy inner',
- undefined,
- 'destroy outer',
- undefined
- ]);
+ assert.deepEqual(logs, ['top level', 'inner', 0, 'destroy inner', 0, 'destroy outer', 0]);
}
});
diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-discouraged/_config.js b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-discouraged/_config.js
index 62c6961242..8452661026 100644
--- a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-discouraged/_config.js
+++ b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-discouraged/_config.js
@@ -10,7 +10,7 @@ export default test({
test({ assert, target, warnings }) {
const warning =
- 'Counter.svelte mutated a value owned by main.svelte. This is strongly discouraged. Consider passing values to child components with `bind:`, or use a callback instead';
+ 'Mutating unbound props (`object`, at Counter.svelte:5:23) is strongly discouraged. Consider using `bind:object={...}` in main.svelte (or using a callback) instead';
const [btn1, btn2] = target.querySelectorAll('button');
btn1.click();
diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-global-2/child.svelte b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-global-2/child.svelte
deleted file mode 100644
index 13de753647..0000000000
--- a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-global-2/child.svelte
+++ /dev/null
@@ -1,18 +0,0 @@
-
diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-global-2/main.svelte b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-global-2/main.svelte
deleted file mode 100644
index 8a6922e9e2..0000000000
--- a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-global-2/main.svelte
+++ /dev/null
@@ -1,5 +0,0 @@
-
diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner/_config.js b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-1/_config.js
similarity index 74%
rename from packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner/_config.js
rename to packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-1/_config.js
index c07b9ce129..96b18d1854 100644
--- a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner/_config.js
+++ b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-1/_config.js
@@ -8,7 +8,7 @@ let warn;
let warnings = [];
export default test({
- html: `clicks: 0 `,
+ html: `[] `,
compileOptions: {
dev: true
@@ -34,8 +34,8 @@ export default test({
btn?.click();
});
- assert.htmlEqual(target.innerHTML, `clicks: 1 `);
+ assert.htmlEqual(target.innerHTML, `[foo] `);
- assert.deepEqual(warnings, []);
+ assert.deepEqual(warnings, [], 'expected getContext to have widened ownership');
}
});
diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-1/main.svelte b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-1/main.svelte
new file mode 100644
index 0000000000..2dd7cab141
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-1/main.svelte
@@ -0,0 +1,9 @@
+
+
+
diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-3/sub.svelte b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-1/sub.svelte
similarity index 100%
rename from packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-3/sub.svelte
rename to packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-1/sub.svelte
diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-2/_config.js b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-2/_config.js
index c07b9ce129..66f1726a2a 100644
--- a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-2/_config.js
+++ b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-2/_config.js
@@ -1,41 +1,24 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
-/** @type {typeof console.warn} */
-let warn;
-
-/** @type {any[]} */
-let warnings = [];
-
export default test({
- html: `clicks: 0 `,
-
compileOptions: {
dev: true
},
- before_test: () => {
- warn = console.warn;
-
- console.warn = (...args) => {
- warnings.push(...args);
- };
- },
+ test({ assert, target, warnings }) {
+ const [btn1, btn2] = target.querySelectorAll('button');
- after_test: () => {
- console.warn = warn;
- warnings = [];
- },
+ flushSync(() => {
+ btn1.click();
+ });
- test({ assert, target }) {
- const btn = target.querySelector('button');
+ assert.deepEqual(warnings.length, 0);
flushSync(() => {
- btn?.click();
+ btn2.click();
});
- assert.htmlEqual(target.innerHTML, `clicks: 1 `);
-
- assert.deepEqual(warnings, []);
+ assert.deepEqual(warnings.length, 1);
}
});
diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-2/main.svelte b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-2/main.svelte
index ad450a937e..0be7e434e4 100644
--- a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-2/main.svelte
+++ b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-2/main.svelte
@@ -1,9 +1,8 @@
-
- global.a.b += 1}>
- clicks: {global.a.b}
-
+
diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-2/state.svelte.js b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-2/state.svelte.js
index 3e7a68cf97..2906b9bce5 100644
--- a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-2/state.svelte.js
+++ b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-2/state.svelte.js
@@ -1 +1,14 @@
-export let global = $state({});
+export function create_my_state() {
+ const my_state = $state({
+ a: 0
+ });
+
+ function inc() {
+ my_state.a++;
+ }
+
+ return {
+ my_state,
+ inc
+ };
+}
diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-5/sub.svelte b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-2/sub.svelte
similarity index 100%
rename from packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-5/sub.svelte
rename to packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-2/sub.svelte
diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-7/Child.svelte b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-3/Child.svelte
similarity index 100%
rename from packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-7/Child.svelte
rename to packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-3/Child.svelte
diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-3/_config.js b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-3/_config.js
index 96b18d1854..ab7327ab8b 100644
--- a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-3/_config.js
+++ b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-3/_config.js
@@ -1,41 +1,24 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
-/** @type {typeof console.warn} */
-let warn;
-
-/** @type {any[]} */
-let warnings = [];
-
export default test({
- html: `[] `,
-
compileOptions: {
dev: true
},
- before_test: () => {
- warn = console.warn;
-
- console.warn = (...args) => {
- warnings.push(...args);
- };
- },
+ async test({ assert, target, warnings }) {
+ const [btn1, btn2] = target.querySelectorAll('button');
- after_test: () => {
- console.warn = warn;
- warnings = [];
- },
+ flushSync(() => {
+ btn1.click();
+ });
- test({ assert, target }) {
- const btn = target.querySelector('button');
+ assert.deepEqual(warnings.length, 0);
flushSync(() => {
- btn?.click();
+ btn2.click();
});
- assert.htmlEqual(target.innerHTML, `[foo] `);
-
- assert.deepEqual(warnings, [], 'expected getContext to have widened ownership');
+ assert.deepEqual(warnings.length, 1);
}
});
diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-3/main.svelte b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-3/main.svelte
index 2dd7cab141..8e8343790b 100644
--- a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-3/main.svelte
+++ b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-3/main.svelte
@@ -1,9 +1,13 @@
-
+First click here
+
diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-4/_config.js b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-4/_config.js
deleted file mode 100644
index aeb3740dfe..0000000000
--- a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-4/_config.js
+++ /dev/null
@@ -1,37 +0,0 @@
-import { flushSync } from 'svelte';
-import { test } from '../../test';
-
-/** @type {typeof console.warn} */
-let warn;
-
-/** @type {any[]} */
-let warnings = [];
-
-export default test({
- compileOptions: {
- dev: true
- },
-
- before_test: () => {
- warn = console.warn;
-
- console.warn = (...args) => {
- warnings.push(...args);
- };
- },
-
- after_test: () => {
- console.warn = warn;
- warnings = [];
- },
-
- test({ assert, target }) {
- const btn = target.querySelector('button');
-
- flushSync(() => {
- btn?.click();
- });
-
- assert.deepEqual(warnings, []);
- }
-});
diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-4/main.svelte b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-4/main.svelte
deleted file mode 100644
index 2d40c13949..0000000000
--- a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-4/main.svelte
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-
-
- global.increment_a_b()}>
- click me
-
diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-4/state.svelte.js b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-4/state.svelte.js
deleted file mode 100644
index 4079059171..0000000000
--- a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-4/state.svelte.js
+++ /dev/null
@@ -1,13 +0,0 @@
-class Global {
- state = $state({});
-
- add_a(a) {
- this.state.a = a;
- }
-
- increment_a_b() {
- this.state.a.b++;
- }
-}
-
-export const global = new Global();
diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-4/sub.svelte b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-4/sub.svelte
deleted file mode 100644
index 044904aa18..0000000000
--- a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-4/sub.svelte
+++ /dev/null
@@ -1,8 +0,0 @@
-
diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-5/_config.js b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-5/_config.js
deleted file mode 100644
index 66f1726a2a..0000000000
--- a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-5/_config.js
+++ /dev/null
@@ -1,24 +0,0 @@
-import { flushSync } from 'svelte';
-import { test } from '../../test';
-
-export default test({
- compileOptions: {
- dev: true
- },
-
- test({ assert, target, warnings }) {
- const [btn1, btn2] = target.querySelectorAll('button');
-
- flushSync(() => {
- btn1.click();
- });
-
- assert.deepEqual(warnings.length, 0);
-
- flushSync(() => {
- btn2.click();
- });
-
- assert.deepEqual(warnings.length, 1);
- }
-});
diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-5/main.svelte b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-5/main.svelte
deleted file mode 100644
index 0be7e434e4..0000000000
--- a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-5/main.svelte
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-5/state.svelte.js b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-5/state.svelte.js
deleted file mode 100644
index 2906b9bce5..0000000000
--- a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-5/state.svelte.js
+++ /dev/null
@@ -1,14 +0,0 @@
-export function create_my_state() {
- const my_state = $state({
- a: 0
- });
-
- function inc() {
- my_state.a++;
- }
-
- return {
- my_state,
- inc
- };
-}
diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-6/Child.svelte b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-6/Child.svelte
deleted file mode 100644
index aa31fd7606..0000000000
--- a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-6/Child.svelte
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
- {
- foo.person.name.last = 'T';
-}}>mutate
diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-6/_config.js b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-6/_config.js
deleted file mode 100644
index cc9ea715f0..0000000000
--- a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-6/_config.js
+++ /dev/null
@@ -1,37 +0,0 @@
-import { flushSync } from 'svelte';
-import { test } from '../../test';
-
-/** @type {typeof console.warn} */
-let warn;
-
-/** @type {any[]} */
-let warnings = [];
-
-export default test({
- compileOptions: {
- dev: true
- },
-
- before_test: () => {
- warn = console.warn;
-
- console.warn = (...args) => {
- warnings.push(...args);
- };
- },
-
- after_test: () => {
- console.warn = warn;
- warnings = [];
- },
-
- async test({ assert, target }) {
- const btn = target.querySelector('button');
-
- flushSync(() => {
- btn?.click();
- });
-
- assert.deepEqual(warnings.length, 0);
- }
-});
diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-6/main.svelte b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-6/main.svelte
deleted file mode 100644
index 92d7dbd2db..0000000000
--- a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-6/main.svelte
+++ /dev/null
@@ -1,17 +0,0 @@
-
-
-
diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-7/_config.js b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-7/_config.js
deleted file mode 100644
index ab7327ab8b..0000000000
--- a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-7/_config.js
+++ /dev/null
@@ -1,24 +0,0 @@
-import { flushSync } from 'svelte';
-import { test } from '../../test';
-
-export default test({
- compileOptions: {
- dev: true
- },
-
- async test({ assert, target, warnings }) {
- const [btn1, btn2] = target.querySelectorAll('button');
-
- flushSync(() => {
- btn1.click();
- });
-
- assert.deepEqual(warnings.length, 0);
-
- flushSync(() => {
- btn2.click();
- });
-
- assert.deepEqual(warnings.length, 1);
- }
-});
diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-7/main.svelte b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-7/main.svelte
deleted file mode 100644
index 8e8343790b..0000000000
--- a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-7/main.svelte
+++ /dev/null
@@ -1,13 +0,0 @@
-
-
-First click here
-
diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner/Counter.svelte b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner/Counter.svelte
deleted file mode 100644
index ffe6ef75c4..0000000000
--- a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner/Counter.svelte
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
- global.object.count += 1}>
- clicks: {global.object.count}
-
diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner/main.svelte b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner/main.svelte
deleted file mode 100644
index 5f1c7461f6..0000000000
--- a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner/main.svelte
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner/state.svelte.js b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner/state.svelte.js
deleted file mode 100644
index 6881c2faf6..0000000000
--- a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner/state.svelte.js
+++ /dev/null
@@ -1,3 +0,0 @@
-export let global = $state({
- object: { count: -1 }
-});
diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-ok/_config.js b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-ok/_config.js
new file mode 100644
index 0000000000..437385e185
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-ok/_config.js
@@ -0,0 +1,18 @@
+import { flushSync } from 'svelte';
+import { test } from '../../test';
+
+export default test({
+ compileOptions: {
+ dev: true
+ },
+
+ test({ assert, target, warnings }) {
+ const btn = target.querySelector('button');
+ btn?.click();
+ flushSync();
+
+ assert.deepEqual(warnings, []);
+ },
+
+ warnings: []
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-ok/child.svelte b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-ok/child.svelte
new file mode 100644
index 0000000000..0243a6c7d1
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-ok/child.svelte
@@ -0,0 +1,8 @@
+
+
+ {
+ klass.y = 2;
+ getter_setter.y = 2;
+}}>mutate
diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-ok/main.svelte b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-ok/main.svelte
new file mode 100644
index 0000000000..8685664ab1
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-ok/main.svelte
@@ -0,0 +1,21 @@
+
+
+
diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-with-binding-2/_config.js b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-with-binding-2/_config.js
index 87474a05cc..39fa80c55a 100644
--- a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-with-binding-2/_config.js
+++ b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-with-binding-2/_config.js
@@ -8,6 +8,6 @@ export default test({
},
warnings: [
- 'Intermediate.svelte passed a value to Counter.svelte with `bind:`, but the value is owned by main.svelte. Consider creating a binding between main.svelte and Intermediate.svelte'
+ 'Intermediate.svelte passed property `object` to Counter.svelte with `bind:`, but its parent component main.svelte did not declare `object` as a binding. Consider creating a binding between main.svelte and Intermediate.svelte (e.g. `bind:object={...}` instead of `object={...}`)'
]
});
diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-with-binding-3/_config.js b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-with-binding-3/_config.js
index 66e5184380..7b8cc676d5 100644
--- a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-with-binding-3/_config.js
+++ b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-with-binding-3/_config.js
@@ -33,7 +33,7 @@ export default test({
assert.htmlEqual(target.innerHTML, `clicks: 1 clicks: 1 `);
assert.deepEqual(warnings, [
- 'Counter.svelte mutated a value owned by main.svelte. This is strongly discouraged. Consider passing values to child components with `bind:`, or use a callback instead'
+ 'Mutating unbound props (`notshared`, at Counter.svelte:10:23) is strongly discouraged. Consider using `bind:notshared={...}` in main.svelte (or using a callback) instead'
]);
}
});
diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-with-binding-7/_config.js b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-with-binding-7/_config.js
index e766a946d0..bd2ecc28b6 100644
--- a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-with-binding-7/_config.js
+++ b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-with-binding-7/_config.js
@@ -1,7 +1,6 @@
import { flushSync } from 'svelte';
import { ok, test } from '../../test';
-// Tests that proxies widen ownership correctly even if not directly connected to each other
export default test({
compileOptions: {
dev: true
diff --git a/packages/svelte/tests/runtime-runes/samples/onmount-prop-access/Component.svelte b/packages/svelte/tests/runtime-runes/samples/onmount-prop-access/Component.svelte
new file mode 100644
index 0000000000..b5da702fa7
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/onmount-prop-access/Component.svelte
@@ -0,0 +1,12 @@
+
diff --git a/packages/svelte/tests/runtime-runes/samples/onmount-prop-access/_config.js b/packages/svelte/tests/runtime-runes/samples/onmount-prop-access/_config.js
new file mode 100644
index 0000000000..0d24e265d3
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/onmount-prop-access/_config.js
@@ -0,0 +1,7 @@
+import { test } from '../../test';
+
+export default test({
+ async test({ assert, logs }) {
+ assert.deepEqual(logs, [1]);
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/onmount-prop-access/main.svelte b/packages/svelte/tests/runtime-runes/samples/onmount-prop-access/main.svelte
new file mode 100644
index 0000000000..92746760a4
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/onmount-prop-access/main.svelte
@@ -0,0 +1,14 @@
+
+
+{#key key}
+
+{/key}
diff --git a/packages/svelte/tests/runtime-runes/samples/ownership-invalid-binding-bindable-fallback/Child.svelte b/packages/svelte/tests/runtime-runes/samples/ownership-invalid-binding-bindable-fallback/Child.svelte
new file mode 100644
index 0000000000..78b82caed9
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/ownership-invalid-binding-bindable-fallback/Child.svelte
@@ -0,0 +1,5 @@
+
+
+{test}
diff --git a/packages/svelte/tests/runtime-runes/samples/ownership-invalid-binding-bindable-fallback/Parent.svelte b/packages/svelte/tests/runtime-runes/samples/ownership-invalid-binding-bindable-fallback/Parent.svelte
new file mode 100644
index 0000000000..7bfb17aa64
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/ownership-invalid-binding-bindable-fallback/Parent.svelte
@@ -0,0 +1,7 @@
+
+
+
diff --git a/packages/svelte/tests/runtime-runes/samples/ownership-invalid-binding-bindable-fallback/_config.js b/packages/svelte/tests/runtime-runes/samples/ownership-invalid-binding-bindable-fallback/_config.js
new file mode 100644
index 0000000000..e93067eb9d
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/ownership-invalid-binding-bindable-fallback/_config.js
@@ -0,0 +1,11 @@
+import { test } from '../../test';
+
+export default test({
+ mode: ['client'],
+ compileOptions: {
+ dev: true
+ },
+ async test({ warnings, assert }) {
+ assert.deepEqual(warnings, []);
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/ownership-invalid-binding-bindable-fallback/main.svelte b/packages/svelte/tests/runtime-runes/samples/ownership-invalid-binding-bindable-fallback/main.svelte
new file mode 100644
index 0000000000..282afb1771
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/ownership-invalid-binding-bindable-fallback/main.svelte
@@ -0,0 +1,5 @@
+
+
+
diff --git a/packages/svelte/tests/runtime-runes/samples/ownership-invalid-mutation-bindable-fallback/Parent.svelte b/packages/svelte/tests/runtime-runes/samples/ownership-invalid-mutation-bindable-fallback/Parent.svelte
new file mode 100644
index 0000000000..7d6b248da7
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/ownership-invalid-mutation-bindable-fallback/Parent.svelte
@@ -0,0 +1,8 @@
+
+
+test = {}}>
+test.test = {}}>
+
+{test}
\ No newline at end of file
diff --git a/packages/svelte/tests/runtime-runes/samples/ownership-invalid-mutation-bindable-fallback/_config.js b/packages/svelte/tests/runtime-runes/samples/ownership-invalid-mutation-bindable-fallback/_config.js
new file mode 100644
index 0000000000..9b4e3479ea
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/ownership-invalid-mutation-bindable-fallback/_config.js
@@ -0,0 +1,23 @@
+import { flushSync } from 'svelte';
+import { test } from '../../test';
+
+export default test({
+ mode: ['client'],
+ compileOptions: {
+ dev: true
+ },
+ async test({ warnings, assert, target }) {
+ const [btn, btn2] = target.querySelectorAll('button');
+ flushSync(() => {
+ btn2.click();
+ });
+ assert.deepEqual(warnings, []);
+ flushSync(() => {
+ btn.click();
+ });
+ flushSync(() => {
+ btn2.click();
+ });
+ assert.deepEqual(warnings, []);
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/ownership-invalid-mutation-bindable-fallback/main.svelte b/packages/svelte/tests/runtime-runes/samples/ownership-invalid-mutation-bindable-fallback/main.svelte
new file mode 100644
index 0000000000..282afb1771
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/ownership-invalid-mutation-bindable-fallback/main.svelte
@@ -0,0 +1,5 @@
+
+
+
diff --git a/packages/svelte/tests/runtime-runes/samples/props-id-prefix/Child.svelte b/packages/svelte/tests/runtime-runes/samples/props-id-prefix/Child.svelte
new file mode 100644
index 0000000000..ad8bbd6f01
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/props-id-prefix/Child.svelte
@@ -0,0 +1,5 @@
+
+
+{id}
diff --git a/packages/svelte/tests/runtime-runes/samples/props-id-prefix/_config.js b/packages/svelte/tests/runtime-runes/samples/props-id-prefix/_config.js
new file mode 100644
index 0000000000..6d4306c413
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/props-id-prefix/_config.js
@@ -0,0 +1,60 @@
+import { flushSync } from 'svelte';
+import { test } from '../../test';
+
+export default test({
+ id_prefix: 'myPrefix',
+ test({ assert, target, variant }) {
+ if (variant === 'dom') {
+ assert.htmlEqual(
+ target.innerHTML,
+ `
+ toggle
+ c1
+ c2
+ c3
+ c4
+ `
+ );
+ } else {
+ assert.htmlEqual(
+ target.innerHTML,
+ `
+ toggle
+ myPrefix-s1
+ myPrefix-s2
+ myPrefix-s3
+ myPrefix-s4
+ `
+ );
+ }
+
+ let button = target.querySelector('button');
+ flushSync(() => button?.click());
+
+ if (variant === 'dom') {
+ assert.htmlEqual(
+ target.innerHTML,
+ `
+ toggle
+ c1
+ c2
+ c3
+ c4
+ c5
+ `
+ );
+ } else {
+ assert.htmlEqual(
+ target.innerHTML,
+ `
+ toggle
+ myPrefix-s1
+ myPrefix-s2
+ myPrefix-s3
+ myPrefix-s4
+ c1
+ `
+ );
+ }
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/props-id-prefix/main.svelte b/packages/svelte/tests/runtime-runes/samples/props-id-prefix/main.svelte
new file mode 100644
index 0000000000..646bb2ebde
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/props-id-prefix/main.svelte
@@ -0,0 +1,19 @@
+
+
+ show = !show}>toggle
+
+{id}
+
+
+
+
+
+{#if show}
+
+{/if}
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..416ef6cfbe
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/props-id/_config.js
@@ -0,0 +1,59 @@
+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 {
+ assert.htmlEqual(
+ target.innerHTML,
+ `
+ toggle
+ s1
+ s2
+ s3
+ s4
+ c1
+ `
+ );
+ }
+ }
+});
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/proxy-coercive-assignment-warning/_config.js b/packages/svelte/tests/runtime-runes/samples/proxy-coercive-assignment-warning/_config.js
index f029227472..4462f492fa 100644
--- a/packages/svelte/tests/runtime-runes/samples/proxy-coercive-assignment-warning/_config.js
+++ b/packages/svelte/tests/runtime-runes/samples/proxy-coercive-assignment-warning/_config.js
@@ -6,7 +6,7 @@ export default test({
dev: true
},
- html: `items: null x
`,
+ html: `items: null x
`,
test({ assert, target, warnings }) {
const btn = target.querySelector('button');
@@ -15,13 +15,13 @@ export default test({
flushSync(() => btn.click());
assert.htmlEqual(
target.innerHTML,
- `items: [] x
`
+ `items: [] x
`
);
flushSync(() => btn.click());
assert.htmlEqual(
target.innerHTML,
- `items: [0] x
`
+ `items: [0] x
`
);
const input = target.querySelector('input');
@@ -30,7 +30,7 @@ export default test({
flushSync(() => input.dispatchEvent(new Event('change', { bubbles: true })));
assert.deepEqual(warnings, [
- 'Assignment to `items` property (main.svelte:8:24) will evaluate to the right-hand side, not the value of `items` following the assignment. This may result in unexpected behaviour.'
+ 'Assignment to `items` property (main.svelte:9:24) will evaluate to the right-hand side, not the value of `items` following the assignment. This may result in unexpected behaviour.'
]);
}
});
diff --git a/packages/svelte/tests/runtime-runes/samples/proxy-coercive-assignment-warning/main.svelte b/packages/svelte/tests/runtime-runes/samples/proxy-coercive-assignment-warning/main.svelte
index ad94c4e56e..a79fe873b7 100644
--- a/packages/svelte/tests/runtime-runes/samples/proxy-coercive-assignment-warning/main.svelte
+++ b/packages/svelte/tests/runtime-runes/samples/proxy-coercive-assignment-warning/main.svelte
@@ -3,6 +3,7 @@
let entries = $state([]);
let object = $state({ items: null, group: [] });
+ let elementFunBind = $state();
(object.items ??= []).push(object.items.length)}>
@@ -13,6 +14,12 @@
x
+
entries[2], (v) => (entries[2] = v)}>
+
+{#snippet funBind(context)}
+ {}, (e) => (context.element = e)} />
+{/snippet}
+{@render funBind({ set element(e) { elementFunBind = e } })}
diff --git a/packages/svelte/tests/runtime-runes/samples/random/_config.js b/packages/svelte/tests/runtime-runes/samples/random/_config.js
new file mode 100644
index 0000000000..368dd20c6c
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/random/_config.js
@@ -0,0 +1,8 @@
+import { test } from '../../test';
+
+export default test({
+ test({ assert, target }) {
+ const [p1, p2] = target.querySelectorAll('p');
+ assert.notEqual(p1.textContent, p2.textContent);
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/random/main.svelte b/packages/svelte/tests/runtime-runes/samples/random/main.svelte
new file mode 100644
index 0000000000..e1ec0b5649
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/random/main.svelte
@@ -0,0 +1,6 @@
+
+
+{(Math.random() * m).toFixed(10)}
+{(Math.random() * m).toFixed(10)}
diff --git a/packages/svelte/tests/runtime-runes/samples/runes-from-func/_config.js b/packages/svelte/tests/runtime-runes/samples/runes-from-func/_config.js
index 7d3dd9993f..5ed7579c62 100644
--- a/packages/svelte/tests/runtime-runes/samples/runes-from-func/_config.js
+++ b/packages/svelte/tests/runtime-runes/samples/runes-from-func/_config.js
@@ -1,3 +1,4 @@
+import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
@@ -5,7 +6,7 @@ export default test({
async test({ assert, target }) {
await Promise.resolve();
- await Promise.resolve();
+ flushSync();
assert.htmlEqual(target.innerHTML, `1 `);
}
});
diff --git a/packages/svelte/tests/runtime-runes/samples/select-value-with-call/_config.js b/packages/svelte/tests/runtime-runes/samples/select-value-with-call/_config.js
new file mode 100644
index 0000000000..0597c2fda8
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/select-value-with-call/_config.js
@@ -0,0 +1,7 @@
+import { test } from '../../test';
+
+export default test({
+ async test({ assert, errors }) {
+ assert.deepEqual(errors, []);
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/select-value-with-call/main.svelte b/packages/svelte/tests/runtime-runes/samples/select-value-with-call/main.svelte
new file mode 100644
index 0000000000..b1d60ecf6d
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/select-value-with-call/main.svelte
@@ -0,0 +1,6 @@
+
+
+
+
\ No newline at end of file
diff --git a/packages/svelte/tests/runtime-runes/samples/snippet-block-without-render-tag-dev/_config.js b/packages/svelte/tests/runtime-runes/samples/snippet-block-without-render-tag-dev/_config.js
new file mode 100644
index 0000000000..94c5de10af
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/snippet-block-without-render-tag-dev/_config.js
@@ -0,0 +1,8 @@
+import { test } from '../../test';
+
+export default test({
+ compileOptions: {
+ dev: true
+ },
+ runtime_error: 'snippet_without_render_tag'
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/snippet-block-without-render-tag-dev/main.svelte b/packages/svelte/tests/runtime-runes/samples/snippet-block-without-render-tag-dev/main.svelte
new file mode 100644
index 0000000000..3f8edfe4fa
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/snippet-block-without-render-tag-dev/main.svelte
@@ -0,0 +1,5 @@
+{testSnippet}
+
+{#snippet testSnippet()}
+ hi again
+{/snippet}
diff --git a/packages/svelte/tests/snapshot/_config.js b/packages/svelte/tests/runtime-runes/samples/snippet-block-without-render-tag-prod/_config.js
similarity index 100%
rename from packages/svelte/tests/snapshot/_config.js
rename to packages/svelte/tests/runtime-runes/samples/snippet-block-without-render-tag-prod/_config.js
diff --git a/packages/svelte/tests/runtime-runes/samples/snippet-block-without-render-tag-prod/main.svelte b/packages/svelte/tests/runtime-runes/samples/snippet-block-without-render-tag-prod/main.svelte
new file mode 100644
index 0000000000..3f8edfe4fa
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/snippet-block-without-render-tag-prod/main.svelte
@@ -0,0 +1,5 @@
+{testSnippet}
+
+{#snippet testSnippet()}
+ hi again
+{/snippet}
diff --git a/packages/svelte/tests/runtime-runes/samples/snippet-children-without-render-tag-dev-prod/_config.js b/packages/svelte/tests/runtime-runes/samples/snippet-children-without-render-tag-dev-prod/_config.js
new file mode 100644
index 0000000000..f47bee71df
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/snippet-children-without-render-tag-dev-prod/_config.js
@@ -0,0 +1,3 @@
+import { test } from '../../test';
+
+export default test({});
diff --git a/packages/svelte/tests/runtime-runes/samples/snippet-children-without-render-tag-dev-prod/main.svelte b/packages/svelte/tests/runtime-runes/samples/snippet-children-without-render-tag-dev-prod/main.svelte
new file mode 100644
index 0000000000..4a4ed3176f
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/snippet-children-without-render-tag-dev-prod/main.svelte
@@ -0,0 +1,5 @@
+
+
+Hi
diff --git a/packages/svelte/tests/runtime-runes/samples/snippet-children-without-render-tag-dev-prod/unrendered-children.svelte b/packages/svelte/tests/runtime-runes/samples/snippet-children-without-render-tag-dev-prod/unrendered-children.svelte
new file mode 100644
index 0000000000..6b7154a5a4
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/snippet-children-without-render-tag-dev-prod/unrendered-children.svelte
@@ -0,0 +1,5 @@
+
+
+{children}
diff --git a/packages/svelte/tests/runtime-runes/samples/snippet-children-without-render-tag-dev/_config.js b/packages/svelte/tests/runtime-runes/samples/snippet-children-without-render-tag-dev/_config.js
new file mode 100644
index 0000000000..94c5de10af
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/snippet-children-without-render-tag-dev/_config.js
@@ -0,0 +1,8 @@
+import { test } from '../../test';
+
+export default test({
+ compileOptions: {
+ dev: true
+ },
+ runtime_error: 'snippet_without_render_tag'
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/snippet-children-without-render-tag-dev/main.svelte b/packages/svelte/tests/runtime-runes/samples/snippet-children-without-render-tag-dev/main.svelte
new file mode 100644
index 0000000000..4a4ed3176f
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/snippet-children-without-render-tag-dev/main.svelte
@@ -0,0 +1,5 @@
+
+
+Hi
diff --git a/packages/svelte/tests/runtime-runes/samples/snippet-children-without-render-tag-dev/unrendered-children.svelte b/packages/svelte/tests/runtime-runes/samples/snippet-children-without-render-tag-dev/unrendered-children.svelte
new file mode 100644
index 0000000000..6b7154a5a4
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/snippet-children-without-render-tag-dev/unrendered-children.svelte
@@ -0,0 +1,5 @@
+
+
+{children}
diff --git a/packages/svelte/tests/runtime-runes/samples/store-reassign-object/_config.js b/packages/svelte/tests/runtime-runes/samples/store-reassign-object/_config.js
new file mode 100644
index 0000000000..f9a329889d
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/store-reassign-object/_config.js
@@ -0,0 +1,7 @@
+import { test } from '../../test';
+
+export default test({
+ async test({ target, assert }) {
+ assert.htmlEqual(target.innerHTML, `bar
`);
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/store-reassign-object/main.svelte b/packages/svelte/tests/runtime-runes/samples/store-reassign-object/main.svelte
new file mode 100644
index 0000000000..ecffbb2d83
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/store-reassign-object/main.svelte
@@ -0,0 +1,11 @@
+
+
+{clone.name}
diff --git a/packages/svelte/tests/runtime-runes/samples/style-directive-mutations/_config.js b/packages/svelte/tests/runtime-runes/samples/style-directive-mutations/_config.js
new file mode 100644
index 0000000000..bd76e4e6b9
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/style-directive-mutations/_config.js
@@ -0,0 +1,95 @@
+import { flushSync, tick } from 'svelte';
+import { test } from '../../test';
+
+// This test counts mutations on hydration
+// set_style() should not mutate style on hydration, except if mismatch
+export default test({
+ mode: ['server', 'hydrate'],
+
+ server_props: {
+ browser: false
+ },
+
+ props: {
+ browser: true
+ },
+
+ ssrHtml: `
+
+
+
+
+
+
+
+
+
+
+
+ `,
+
+ html: `
+
+
+
+
+
+
+
+
+
+
+
+ `,
+
+ async test({ target, assert, component, instance }) {
+ flushSync();
+ tick();
+ assert.deepEqual(instance.get_and_clear_mutations(), ['MAIN']);
+
+ let divs = target.querySelectorAll('div');
+
+ // Note : we cannot compare HTML because set_style() use dom.style.cssText
+ // which can alter the format of the attribute...
+
+ divs.forEach((d) => assert.equal(d.style.margin, ''));
+ divs.forEach((d) => assert.equal(d.style.color, 'red'));
+ divs.forEach((d) => assert.equal(d.style.fontSize, '18px'));
+
+ component.margin = '1px';
+ flushSync();
+ assert.deepEqual(
+ instance.get_and_clear_mutations(),
+ ['DIV', 'DIV', 'DIV', 'DIV', 'DIV', 'DIV', 'DIV', 'DIV', 'DIV'],
+ 'margin'
+ );
+ divs.forEach((d) => assert.equal(d.style.margin, '1px'));
+
+ component.color = 'yellow';
+ flushSync();
+ assert.deepEqual(
+ instance.get_and_clear_mutations(),
+ ['DIV', 'DIV', 'DIV', 'DIV', 'DIV', 'DIV', 'DIV', 'DIV', 'DIV'],
+ 'color'
+ );
+ divs.forEach((d) => assert.equal(d.style.color, 'yellow'));
+
+ component.fontSize = '10px';
+ flushSync();
+ assert.deepEqual(
+ instance.get_and_clear_mutations(),
+ ['DIV', 'DIV', 'DIV', 'DIV', 'DIV', 'DIV', 'DIV', 'DIV', 'DIV'],
+ 'fontSize'
+ );
+ divs.forEach((d) => assert.equal(d.style.fontSize, '10px'));
+
+ component.fontSize = null;
+ flushSync();
+ assert.deepEqual(
+ instance.get_and_clear_mutations(),
+ ['DIV', 'DIV', 'DIV', 'DIV', 'DIV', 'DIV', 'DIV', 'DIV', 'DIV'],
+ 'fontSize'
+ );
+ divs.forEach((d) => assert.equal(d.style.fontSize, ''));
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/style-directive-mutations/main.svelte b/packages/svelte/tests/runtime-runes/samples/style-directive-mutations/main.svelte
new file mode 100644
index 0000000000..ae4da8ae37
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/style-directive-mutations/main.svelte
@@ -0,0 +1,54 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/svelte/tests/runtime-runes/samples/style-update/_config.js b/packages/svelte/tests/runtime-runes/samples/style-update/_config.js
new file mode 100644
index 0000000000..52690a431a
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/style-update/_config.js
@@ -0,0 +1,65 @@
+import { flushSync } from 'svelte';
+import { test } from '../../test';
+
+const style_1 = 'invalid-key:0; margin:4px;;color: green ;color:blue ';
+const style_2 = ' other-key : 0 ; padding:2px; COLOR:green; color: blue';
+const style_2_normalized = 'padding: 2px; color: blue;';
+
+// https://github.com/sveltejs/svelte/issues/15309
+export default test({
+ props: {
+ style: style_1
+ },
+
+ ssrHtml: `
+
+
+
+
+
+ `,
+
+ async test({ assert, target, component }) {
+ component.style = style_2;
+ flushSync();
+
+ assert.htmlEqual(
+ target.innerHTML,
+ `
+
+
+
+
+
+ `
+ );
+
+ component.style = '';
+ flushSync();
+
+ assert.htmlEqual(
+ target.innerHTML,
+ `
+
+
+
+
+
+ `
+ );
+
+ component.style = null;
+ flushSync();
+
+ assert.htmlEqual(
+ target.innerHTML,
+ `
+
+
+
+
+
+ `
+ );
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/style-update/main.svelte b/packages/svelte/tests/runtime-runes/samples/style-update/main.svelte
new file mode 100644
index 0000000000..d29590d670
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/style-update/main.svelte
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
diff --git a/packages/svelte/tests/runtime-runes/samples/svelte-boundary-dev-const/_config.js b/packages/svelte/tests/runtime-runes/samples/svelte-boundary-dev-const/_config.js
new file mode 100644
index 0000000000..3c0195ce34
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/svelte-boundary-dev-const/_config.js
@@ -0,0 +1,33 @@
+import { flushSync } from 'svelte';
+import { test } from '../../test';
+
+// https://github.com/sveltejs/svelte/issues/15368
+export default test({
+ compileOptions: {
+ dev: true
+ },
+
+ mode: ['client'],
+
+ html: `
+ BOOM
+ BOOM
+ OK
+ OK
+ `,
+
+ async test({ target, assert, component }) {
+ component.ok = false;
+ flushSync();
+
+ assert.htmlEqual(
+ target.innerHTML,
+ `
+ BOOM
+ BOOM
+ BOOM
+ BOOM
+ `
+ );
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/svelte-boundary-dev-const/main.svelte b/packages/svelte/tests/runtime-runes/samples/svelte-boundary-dev-const/main.svelte
new file mode 100644
index 0000000000..30e074c762
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/svelte-boundary-dev-const/main.svelte
@@ -0,0 +1,49 @@
+
+
+
+ {throwError()}
+
+ {#snippet failed()}
+ BOOM
+ {/snippet}
+
+
+
+ {@const result = throwError()}
+ {result}
+
+ {#snippet failed()}
+ BOOM
+ {/snippet}
+
+
+
+ {throwErrorOnUpdate()}
+
+ {#snippet failed()}
+ BOOM
+ {/snippet}
+
+
+
+ {@const result = throwErrorOnUpdate()}
+ {result}
+
+ {#snippet failed()}
+ BOOM
+ {/snippet}
+
diff --git a/packages/svelte/tests/runtime-runes/samples/svelte-element-css-hash/_config.js b/packages/svelte/tests/runtime-runes/samples/svelte-element-css-hash/_config.js
new file mode 100644
index 0000000000..c7a6213d82
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/svelte-element-css-hash/_config.js
@@ -0,0 +1,60 @@
+import { flushSync } from 'svelte';
+import { ok, test } from '../../test';
+
+export default test({
+ html: `
+
+
+
+
+
+
+ `,
+
+ async test({ assert, target, component }) {
+ component.active = true;
+ flushSync();
+
+ assert.htmlEqual(
+ target.innerHTML,
+ `
+
+
+
+
+
+
+ `
+ );
+
+ component.tag = 'span';
+ flushSync();
+
+ assert.htmlEqual(
+ target.innerHTML,
+ `
+
+
+
+
+
+
+ `
+ );
+
+ component.active = false;
+ flushSync();
+
+ assert.htmlEqual(
+ target.innerHTML,
+ `
+
+
+
+
+
+
+ `
+ );
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/svelte-element-css-hash/main.svelte b/packages/svelte/tests/runtime-runes/samples/svelte-element-css-hash/main.svelte
new file mode 100644
index 0000000000..709f7b4fa1
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/svelte-element-css-hash/main.svelte
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/svelte/tests/runtime-runes/samples/toStore-subscribe2/_config.js b/packages/svelte/tests/runtime-runes/samples/toStore-subscribe2/_config.js
new file mode 100644
index 0000000000..bc1793e7a4
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/toStore-subscribe2/_config.js
@@ -0,0 +1,16 @@
+import { flushSync } from 'svelte';
+import { test } from '../../test';
+
+export default test({
+ async test({ assert, target }) {
+ let btn = target.querySelector('button');
+
+ btn?.click();
+ flushSync();
+
+ assert.htmlEqual(
+ target.innerHTML,
+ `Count 1!
Count from store 1!
Add 1 `
+ );
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/toStore-subscribe2/main.svelte b/packages/svelte/tests/runtime-runes/samples/toStore-subscribe2/main.svelte
new file mode 100644
index 0000000000..82d20105b8
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/toStore-subscribe2/main.svelte
@@ -0,0 +1,11 @@
+
+
+Count {counter}!
+Count from store {$count}!
+
+ counter+=1}>Add 1
diff --git a/packages/svelte/tests/runtime-runes/samples/toStore-teardown/_config.js b/packages/svelte/tests/runtime-runes/samples/toStore-teardown/_config.js
new file mode 100644
index 0000000000..95904f011f
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/toStore-teardown/_config.js
@@ -0,0 +1,13 @@
+import { flushSync } from 'svelte';
+import { test } from '../../test';
+
+export default test({
+ async test({ assert, target }) {
+ let [, btn2] = target.querySelectorAll('button');
+
+ btn2.click();
+ flushSync();
+
+ assert.htmlEqual(target.innerHTML, `Set data Clear data `);
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/toStore-teardown/child.svelte b/packages/svelte/tests/runtime-runes/samples/toStore-teardown/child.svelte
new file mode 100644
index 0000000000..f1b1b7b497
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/toStore-teardown/child.svelte
@@ -0,0 +1,11 @@
+
+
+
+ Current value:
+ {$currentValue}
+
diff --git a/packages/svelte/tests/runtime-runes/samples/toStore-teardown/main.svelte b/packages/svelte/tests/runtime-runes/samples/toStore-teardown/main.svelte
new file mode 100644
index 0000000000..7d36dd95cb
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/toStore-teardown/main.svelte
@@ -0,0 +1,15 @@
+
+
+Set data
+Clear data
+
+{#if data}
+
+{/if}
diff --git a/packages/svelte/tests/runtime-runes/samples/transition-each-4/_config.js b/packages/svelte/tests/runtime-runes/samples/transition-each-4/_config.js
new file mode 100644
index 0000000000..a0e29ec8ff
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/transition-each-4/_config.js
@@ -0,0 +1,35 @@
+import { flushSync } from '../../../../src/index-client.js';
+import { test } from '../../test';
+
+export default test({
+ async test({ assert, raf, target, logs }) {
+ assert.htmlEqual(
+ target.innerHTML,
+ 'Toggle '
+ );
+
+ const btn1 = target.querySelector('button');
+ btn1?.click();
+ flushSync();
+ raf.tick(250);
+
+ assert.htmlEqual(
+ target.innerHTML,
+ 'Toggle '
+ );
+
+ logs.length = 0;
+
+ await Promise.resolve();
+
+ flushSync();
+ raf.tick(500);
+
+ assert.htmlEqual(
+ target.innerHTML,
+ 'Toggle '
+ );
+
+ assert.deepEqual(logs, ['$effect.pre', '$effect.pre', '$effect', '$effect']);
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/transition-each-4/main.svelte b/packages/svelte/tests/runtime-runes/samples/transition-each-4/main.svelte
new file mode 100644
index 0000000000..d399bee691
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/transition-each-4/main.svelte
@@ -0,0 +1,39 @@
+
+
+Toggle
+
+{#if toggle}
+
+ {#each items as item (item)}
+ {(() => {
+ $effect(() => {
+ items;
+ console.log('$effect');
+ });
+
+ $effect.pre(() => {
+ items;
+ console.log('$effect.pre');
+ });
+ })()}
+
{item}
+ {/each}
+
+{/if}
diff --git a/packages/svelte/tests/runtime-runes/samples/typescript/main.svelte b/packages/svelte/tests/runtime-runes/samples/typescript/main.svelte
index cd23b31096..d1b6452df4 100644
--- a/packages/svelte/tests/runtime-runes/samples/typescript/main.svelte
+++ b/packages/svelte/tests/runtime-runes/samples/typescript/main.svelte
@@ -8,7 +8,7 @@
console.log(this);
}
- function foo(): string {
+ function foo(): string {
return ""!;
}
@@ -22,6 +22,11 @@
class MyClass implements Hello {}
+ abstract class MyAbstractClass {
+ abstract x(): void;
+ y() {}
+ }
+
declare const declared_const: number;
declare function declared_fn(): void;
declare class declared_class {
@@ -40,6 +45,8 @@
export type { Hello };
const TypedFoo = Foo;
+ const typeAssertion = true;
+ const x = foo();
+
+
+
+{#snippet snip()}
+ snippet {x}
+{/snippet}
\ No newline at end of file
diff --git a/packages/svelte/tests/runtime-runes/samples/untrack-own-deriveds/_config.js b/packages/svelte/tests/runtime-runes/samples/untrack-own-deriveds/_config.js
new file mode 100644
index 0000000000..18062b86fb
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/untrack-own-deriveds/_config.js
@@ -0,0 +1,20 @@
+import { flushSync } from 'svelte';
+import { test } from '../../test';
+
+export default test({
+ test({ assert, target, logs }) {
+ const button = target.querySelector('button');
+
+ flushSync(() => button?.click());
+
+ assert.htmlEqual(
+ target.innerHTML,
+ `
+ increment
+ 1/2
+ class Foo {
+ value = $state(0);
+ double = $derived(this.value * 2);
+
+ constructor() {
+ console.log(this.value, this.double);
+ }
+
+ increment() {
+ this.value++;
+ }
+ }
+
+ let foo = $state();
+
+ $effect(() => {
+ foo = new Foo();
+ });
+
+
+ foo.increment()}>increment
+
+{#if foo}
+ {foo.value}/{foo.double}
+{/if}
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/untracked-write-pre/_config.js b/packages/svelte/tests/runtime-runes/samples/untracked-write-pre/_config.js
new file mode 100644
index 0000000000..0310ec4fbb
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/untracked-write-pre/_config.js
@@ -0,0 +1,7 @@
+import { test } from '../../test';
+
+export default test({
+ test({ assert, target, logs }) {
+ assert.deepEqual(logs, ['Outer', 'Inner', 'Outer', 'Inner']);
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/untracked-write-pre/main.svelte b/packages/svelte/tests/runtime-runes/samples/untracked-write-pre/main.svelte
new file mode 100644
index 0000000000..5e95dbfd41
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/untracked-write-pre/main.svelte
@@ -0,0 +1,13 @@
+
diff --git a/packages/svelte/tests/runtime-runes/samples/validate-undefined-snippet-default-arg/_config.js b/packages/svelte/tests/runtime-runes/samples/validate-undefined-snippet-default-arg/_config.js
new file mode 100644
index 0000000000..bddb75e677
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/validate-undefined-snippet-default-arg/_config.js
@@ -0,0 +1,5 @@
+import { test } from '../../test';
+
+export default test({
+ html: `default
`
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/validate-undefined-snippet-default-arg/main.svelte b/packages/svelte/tests/runtime-runes/samples/validate-undefined-snippet-default-arg/main.svelte
new file mode 100644
index 0000000000..3f00eba46b
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/validate-undefined-snippet-default-arg/main.svelte
@@ -0,0 +1,5 @@
+{#snippet test(param = "default")}
+ {param}
+{/snippet}
+
+{@render test()}
\ No newline at end of file
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/runtime-runes/samples/writable-derived-2/_config.js b/packages/svelte/tests/runtime-runes/samples/writable-derived-2/_config.js
new file mode 100644
index 0000000000..fde3e7b1ea
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/writable-derived-2/_config.js
@@ -0,0 +1,7 @@
+import { test } from '../../test';
+
+export default test({
+ html: `true true`,
+
+ test({ assert, target, window }) {}
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/writable-derived-2/main.svelte b/packages/svelte/tests/runtime-runes/samples/writable-derived-2/main.svelte
new file mode 100644
index 0000000000..741aa69125
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/writable-derived-2/main.svelte
@@ -0,0 +1,9 @@
+
+
+{expect1} {expect2}
diff --git a/packages/svelte/tests/runtime-runes/samples/writable-derived-2/util.svelte.js b/packages/svelte/tests/runtime-runes/samples/writable-derived-2/util.svelte.js
new file mode 100644
index 0000000000..8e862753ab
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/writable-derived-2/util.svelte.js
@@ -0,0 +1,17 @@
+export const createAppState = (options) => {
+ const source = $derived(options.source());
+ let value = $derived(source);
+
+ return {
+ get value() {
+ return value;
+ },
+ onChange(nextValue) {
+ value = nextValue;
+ }
+ };
+};
+
+const result = createAppState({ source: () => 'wrong' });
+result.onChange('right');
+export const expect2 = result.value === 'right';
diff --git a/packages/svelte/tests/runtime-runes/samples/writable-derived/_config.js b/packages/svelte/tests/runtime-runes/samples/writable-derived/_config.js
new file mode 100644
index 0000000000..b48ccbdfd0
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/writable-derived/_config.js
@@ -0,0 +1,46 @@
+import { flushSync } from 'svelte';
+import { test } from '../../test';
+
+export default test({
+ html: `
+ 0 * 2 = 0
+ `,
+
+ ssrHtml: `
+ 0 * 2 = 0
+ `,
+
+ test({ assert, target, window }) {
+ const [input1, input2] = target.querySelectorAll('input');
+
+ flushSync(() => {
+ input1.value = '10';
+ input1.dispatchEvent(new window.Event('input', { bubbles: true }));
+ });
+
+ assert.htmlEqual(
+ target.innerHTML,
+ `10 * 2 = 20
`
+ );
+
+ flushSync(() => {
+ input2.value = '99';
+ input2.dispatchEvent(new window.Event('input', { bubbles: true }));
+ });
+
+ assert.htmlEqual(
+ target.innerHTML,
+ `10 * 2 = 99
`
+ );
+
+ flushSync(() => {
+ input1.value = '20';
+ input1.dispatchEvent(new window.Event('input', { bubbles: true }));
+ });
+
+ assert.htmlEqual(
+ target.innerHTML,
+ `20 * 2 = 40
`
+ );
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/writable-derived/main.svelte b/packages/svelte/tests/runtime-runes/samples/writable-derived/main.svelte
new file mode 100644
index 0000000000..ab1dde0b9b
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/writable-derived/main.svelte
@@ -0,0 +1,9 @@
+
+
+
+
+
+{count} * 2 = {double}
diff --git a/packages/svelte/tests/server-side-rendering/samples/head-component-props-id/HeadNested.svelte b/packages/svelte/tests/server-side-rendering/samples/head-component-props-id/HeadNested.svelte
new file mode 100644
index 0000000000..0784208798
--- /dev/null
+++ b/packages/svelte/tests/server-side-rendering/samples/head-component-props-id/HeadNested.svelte
@@ -0,0 +1,5 @@
+
+
+
\ No newline at end of file
diff --git a/packages/svelte/tests/server-side-rendering/samples/head-component-props-id/_config.js b/packages/svelte/tests/server-side-rendering/samples/head-component-props-id/_config.js
new file mode 100644
index 0000000000..f47bee71df
--- /dev/null
+++ b/packages/svelte/tests/server-side-rendering/samples/head-component-props-id/_config.js
@@ -0,0 +1,3 @@
+import { test } from '../../test';
+
+export default test({});
diff --git a/packages/svelte/tests/server-side-rendering/samples/head-component-props-id/main.svelte b/packages/svelte/tests/server-side-rendering/samples/head-component-props-id/main.svelte
new file mode 100644
index 0000000000..e32b40e9ed
--- /dev/null
+++ b/packages/svelte/tests/server-side-rendering/samples/head-component-props-id/main.svelte
@@ -0,0 +1,8 @@
+
+
+
+
+
+
diff --git a/packages/svelte/tests/server-side-rendering/test.ts b/packages/svelte/tests/server-side-rendering/test.ts
index f76c5b539f..3e57539427 100644
--- a/packages/svelte/tests/server-side-rendering/test.ts
+++ b/packages/svelte/tests/server-side-rendering/test.ts
@@ -15,6 +15,7 @@ import type { CompileOptions } from '#compiler';
interface SSRTest extends BaseTest {
compileOptions?: Partial;
props?: Record;
+ id_prefix?: string;
withoutNormalizeHtml?: boolean;
errors?: string[];
}
@@ -33,7 +34,7 @@ const { test, run } = suite(async (config, test_dir) => {
const Component = (await import(`${test_dir}/_output/server/main.svelte.js`)).default;
const expected_html = try_read_file(`${test_dir}/_expected.html`);
- const rendered = render(Component, { props: config.props || {} });
+ const rendered = render(Component, { props: config.props || {}, idPrefix: config.id_prefix });
const { body, head } = rendered;
fs.writeFileSync(`${test_dir}/_output/rendered.html`, body);
diff --git a/packages/svelte/tests/signals/test.ts b/packages/svelte/tests/signals/test.ts
index e198a5a89d..3a427e9392 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, [[{}], [{}], [{}], [{}], [{}]]);
+
+ destroy2();
+
+ set(some_state, {});
flushSync();
- assert.deepEqual(log, [[{}], [{}]]);
+
+ 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);
};
});
@@ -381,6 +487,26 @@ describe('signals', () => {
};
});
+ test('schedules rerun when updating deeply nested value', (runes) => {
+ if (!runes) return () => {};
+
+ const value = proxy({ a: { b: { c: 0 } } });
+ user_effect(() => {
+ value.a.b.c += 1;
+ });
+
+ return () => {
+ let errored = false;
+ try {
+ flushSync();
+ } catch (e: any) {
+ assert.include(e.message, 'effect_update_depth_exceeded');
+ errored = true;
+ }
+ assert.equal(errored, true);
+ };
+ });
+
test('schedules rerun when writing to signal before reading it', (runes) => {
if (!runes) return () => {};
@@ -402,6 +528,42 @@ describe('signals', () => {
};
});
+ test('schedules rerun when writing to signal before reading it from derived', (runes) => {
+ if (!runes) return () => {};
+ let log: any[] = [];
+
+ const value = state(1);
+ const double = derived(() => $.get(value) * 2);
+
+ user_effect(() => {
+ set(value, 10);
+ log.push($.get(double));
+ });
+
+ return () => {
+ flushSync();
+ assert.deepEqual(log, [20]);
+ };
+ });
+
+ test('schedules rerun when writing to signal after reading it from derived', (runes) => {
+ if (!runes) return () => {};
+ let log: any[] = [];
+
+ const value = state(1);
+ const double = derived(() => $.get(value) * 2);
+
+ user_effect(() => {
+ log.push($.get(double));
+ set(value, 10);
+ });
+
+ return () => {
+ flushSync();
+ assert.deepEqual(log, [2, 20]);
+ };
+ });
+
test('effect teardown is removed on re-run', () => {
const count = state(0);
let first = true;
@@ -440,6 +602,7 @@ describe('signals', () => {
effect(() => {
log.push('inner', $.get(inner));
});
+ return $.get(outer);
});
});
});
@@ -493,6 +656,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);
@@ -679,6 +939,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 }[]>([]);
@@ -696,14 +978,30 @@ describe('signals', () => {
};
});
- test('deriveds cannot depend on state they own', () => {
+ test('deriveds do not depend on state they own', () => {
return () => {
+ let s;
+
const d = derived(() => {
- const s = state(0);
+ s = state(0);
return $.get(s);
});
- assert.throws(() => $.get(d), 'state_unsafe_local_read');
+ assert.equal($.get(d), 0);
+
+ set(s!, 1);
+ assert.equal($.get(d), 0);
+ };
+ });
+
+ test('effects do not depend on state they own', () => {
+ user_effect(() => {
+ const value = state(0);
+ set(value, $.get(value) + 1);
+ });
+
+ return () => {
+ flushSync();
};
});
@@ -741,29 +1039,119 @@ 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.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.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);
+ };
+ });
+
+ test("deriveds set after they are DIRTY doesn't get updated on get", () => {
+ return () => {
+ const a = state(0);
+ const b = derived(() => $.get(a));
+
+ set(b, 1);
+ assert.equal($.get(b), 1);
+
+ set(a, 2);
+ assert.equal($.get(b), 2);
+ set(b, 3);
+
+ assert.equal($.get(b), 3);
+ };
+ });
+
+ test("unowned deriveds set after they are DIRTY doesn't get updated on get", () => {
+ return () => {
+ const a = state(0);
+ const b = derived(() => $.get(a));
+ const c = derived(() => $.get(b));
+
+ set(b, 1);
+ assert.equal($.get(c), 1);
+
+ set(a, 2);
+
+ assert.equal($.get(b), 2);
+ assert.equal($.get(c), 2);
+ };
+ });
+
+ test('deriveds containing effects work correctly when used with untrack', () => {
+ return () => {
+ let a = render_effect(() => {});
+ let b = state(0);
+ let c;
+ let effects: Effect[] = [];
+
+ const destroy = effect_root(() => {
+ a = render_effect(() => {
+ c = derived(() => {
+ $.untrack(() => {
+ 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);
};
});
@@ -771,14 +1159,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);
};
});
@@ -815,4 +1203,45 @@ describe('signals', () => {
destroy();
};
});
+
+ test('unowned deriveds correctly update', () => {
+ const log: any[] = [];
+
+ return () => {
+ const a = state(0);
+ const b = state(0);
+ const c = derived(() => {
+ return $.get(a);
+ });
+ const d = derived(() => {
+ return $.get(b);
+ });
+
+ const destroy = effect_root(() => {
+ const e = derived(() => {
+ return $.get(c) === 1 && $.get(d) === 1;
+ });
+ render_effect(() => {
+ log.push($.get(e));
+ });
+ });
+
+ assert.deepEqual(log, [false]);
+
+ set(a, 1);
+ set(b, 1);
+
+ flushSync();
+
+ assert.deepEqual(log, [false, true]);
+
+ set(b, 9);
+
+ flushSync();
+
+ assert.deepEqual(log, [false, true, false]);
+
+ destroy();
+ };
+ });
});
diff --git a/packages/svelte/tests/snapshot/samples/await-block-scope/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/await-block-scope/_expected/server/index.svelte.js
index 34e26e7fa0..dd82309a2c 100644
--- a/packages/svelte/tests/snapshot/samples/await-block-scope/_expected/server/index.svelte.js
+++ b/packages/svelte/tests/snapshot/samples/await-block-scope/_expected/server/index.svelte.js
@@ -8,7 +8,7 @@ export default function Await_block_scope($$payload) {
counter.count += 1;
}
- $$payload.out += `clicks: ${$.escape(counter.count)} `;
- $.await(promise?.(), () => {}, (counter) => {}, () => {});
- $$payload.out += ` ${$.escape(counter.count)}`;
-}
\ No newline at end of file
+ $$payload.out += `clicks: ${$.escape(counter.count)} `;
+ $.await($$payload, promise?.(), () => {}, (counter) => {});
+ $$payload.out += ` ${$.escape(counter.count)}`;
+}
diff --git a/packages/svelte/tests/snapshot/samples/bind-component-snippet/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/bind-component-snippet/_expected/client/index.svelte.js
index fa990b33ee..390e86a351 100644
--- a/packages/svelte/tests/snapshot/samples/bind-component-snippet/_expected/client/index.svelte.js
+++ b/packages/svelte/tests/snapshot/samples/bind-component-snippet/_expected/client/index.svelte.js
@@ -23,7 +23,7 @@ export default function Bind_component_snippet($$anchor) {
return $.get(value);
},
set value($$value) {
- $.set(value, $.proxy($$value));
+ $.set(value, $$value, true);
}
});
diff --git a/packages/svelte/tests/snapshot/samples/bind-component-snippet/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/bind-component-snippet/_expected/server/index.svelte.js
index c091179c41..04bfbf6ae4 100644
--- a/packages/svelte/tests/snapshot/samples/bind-component-snippet/_expected/server/index.svelte.js
+++ b/packages/svelte/tests/snapshot/samples/bind-component-snippet/_expected/server/index.svelte.js
@@ -1,9 +1,9 @@
import * as $ from 'svelte/internal/server';
import TextInput from './Child.svelte';
-function snippet($$payload) {
+const snippet = ($$payload) => {
$$payload.out += `Something`;
-}
+};
export default function Bind_component_snippet($$payload) {
let value = '';
@@ -23,7 +23,7 @@ export default function Bind_component_snippet($$payload) {
});
$$payload.out += ` value: ${$.escape(value)}`;
- };
+ }
do {
$$settled = true;
diff --git a/packages/svelte/tests/snapshot/samples/class-state-field-constructor-assignment/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/class-state-field-constructor-assignment/_expected/client/index.svelte.js
index 2898f31a6f..2133974176 100644
--- a/packages/svelte/tests/snapshot/samples/class-state-field-constructor-assignment/_expected/client/index.svelte.js
+++ b/packages/svelte/tests/snapshot/samples/class-state-field-constructor-assignment/_expected/client/index.svelte.js
@@ -12,14 +12,14 @@ export default function Class_state_field_constructor_assignment($$anchor, $$pro
}
set a(value) {
- $.set(this.#a, $.proxy(value));
+ $.set(this.#a, value, true);
}
#b = $.state();
constructor() {
this.a = 1;
- this.#b.v = 2;
+ $.set(this.#b, 2);
}
}
diff --git a/packages/svelte/tests/snapshot/samples/destructured-assignments/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/destructured-assignments/_expected/client/index.svelte.js
index 9651713c52..47f297bce9 100644
--- a/packages/svelte/tests/snapshot/samples/destructured-assignments/_expected/client/index.svelte.js
+++ b/packages/svelte/tests/snapshot/samples/destructured-assignments/_expected/client/index.svelte.js
@@ -8,8 +8,8 @@ let d = 4;
export function update(array) {
(
- $.set(a, $.proxy(array[0])),
- $.set(b, $.proxy(array[1]))
+ $.set(a, array[0], true),
+ $.set(b, array[1], true)
);
[c, d] = array;
diff --git a/packages/svelte/tests/snapshot/samples/dynamic-attributes-casing/_expected/client/main.svelte.js b/packages/svelte/tests/snapshot/samples/dynamic-attributes-casing/_expected/client/main.svelte.js
index ce77a27e19..219db6ffd5 100644
--- a/packages/svelte/tests/snapshot/samples/dynamic-attributes-casing/_expected/client/main.svelte.js
+++ b/packages/svelte/tests/snapshot/samples/dynamic-attributes-casing/_expected/client/main.svelte.js
@@ -11,23 +11,24 @@ export default function Main($$anchor) {
var div = $.first_child(fragment);
var svg = $.sibling(div, 2);
var custom_element = $.sibling(svg, 2);
- var div_1 = $.sibling(custom_element, 2);
- $.template_effect(() => $.set_attribute(div_1, 'foobar', y()));
+ $.template_effect(() => $.set_custom_element_data(custom_element, 'fooBar', x));
+ var div_1 = $.sibling(custom_element, 2);
var svg_1 = $.sibling(div_1, 2);
-
- $.template_effect(() => $.set_attribute(svg_1, 'viewBox', y()));
-
var custom_element_1 = $.sibling(svg_1, 2);
$.template_effect(() => $.set_custom_element_data(custom_element_1, 'fooBar', y()));
- $.template_effect(() => {
- $.set_attribute(div, 'foobar', x);
- $.set_attribute(svg, 'viewBox', x);
- $.set_custom_element_data(custom_element, 'fooBar', x);
- });
+ $.template_effect(
+ ($0, $1) => {
+ $.set_attribute(div, 'foobar', x);
+ $.set_attribute(svg, 'viewBox', x);
+ $.set_attribute(div_1, 'foobar', $0);
+ $.set_attribute(svg_1, 'viewBox', $1);
+ },
+ [y, y]
+ );
$.append($$anchor, fragment);
}
\ No newline at end of file
diff --git a/packages/svelte/tests/snapshot/samples/function-prop-no-getter/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/function-prop-no-getter/_expected/client/index.svelte.js
index c545608bca..762a23754c 100644
--- a/packages/svelte/tests/snapshot/samples/function-prop-no-getter/_expected/client/index.svelte.js
+++ b/packages/svelte/tests/snapshot/samples/function-prop-no-getter/_expected/client/index.svelte.js
@@ -13,7 +13,7 @@ export default function Function_prop_no_getter($$anchor) {
Button($$anchor, {
onmousedown: () => $.set(count, $.get(count) + 1),
onmouseup,
- onmouseenter: () => $.set(count, $.proxy(plusOne($.get(count)))),
+ onmouseenter: () => $.set(count, plusOne($.get(count)), true),
children: ($$anchor, $$slotProps) => {
$.next();
diff --git a/packages/svelte/tests/snapshot/samples/nullish-coallescence-omittance/_config.js b/packages/svelte/tests/snapshot/samples/nullish-coallescence-omittance/_config.js
new file mode 100644
index 0000000000..f47bee71df
--- /dev/null
+++ b/packages/svelte/tests/snapshot/samples/nullish-coallescence-omittance/_config.js
@@ -0,0 +1,3 @@
+import { test } from '../../test';
+
+export default test({});
diff --git a/packages/svelte/tests/snapshot/samples/nullish-coallescence-omittance/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/nullish-coallescence-omittance/_expected/client/index.svelte.js
new file mode 100644
index 0000000000..21f6ed9680
--- /dev/null
+++ b/packages/svelte/tests/snapshot/samples/nullish-coallescence-omittance/_expected/client/index.svelte.js
@@ -0,0 +1,34 @@
+import 'svelte/internal/disclose-version';
+import * as $ from 'svelte/internal/client';
+
+var on_click = (_, count) => $.update(count);
+var root = $.template(` `, 1);
+
+export default function Nullish_coallescence_omittance($$anchor) {
+ let name = 'world';
+ let count = $.state(0);
+ var fragment = root();
+ var h1 = $.first_child(fragment);
+
+ h1.textContent = 'Hello, world!';
+
+ var b = $.sibling(h1, 2);
+
+ b.textContent = '123';
+
+ var button = $.sibling(b, 2);
+
+ button.__click = [on_click, count];
+
+ var text = $.child(button);
+
+ $.reset(button);
+
+ var h1_1 = $.sibling(button, 2);
+
+ h1_1.textContent = 'Hello, world';
+ $.template_effect(() => $.set_text(text, `Count is ${$.get(count) ?? ''}`));
+ $.append($$anchor, fragment);
+}
+
+$.delegate(['click']);
\ No newline at end of file
diff --git a/packages/svelte/tests/snapshot/samples/nullish-coallescence-omittance/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/nullish-coallescence-omittance/_expected/server/index.svelte.js
new file mode 100644
index 0000000000..3b23befcd4
--- /dev/null
+++ b/packages/svelte/tests/snapshot/samples/nullish-coallescence-omittance/_expected/server/index.svelte.js
@@ -0,0 +1,8 @@
+import * as $ from 'svelte/internal/server';
+
+export default function Nullish_coallescence_omittance($$payload) {
+ let name = 'world';
+ let count = 0;
+
+ $$payload.out += `Hello, world! 123 Count is ${$.escape(count)} Hello, world `;
+}
\ No newline at end of file
diff --git a/packages/svelte/tests/snapshot/samples/nullish-coallescence-omittance/index.svelte b/packages/svelte/tests/snapshot/samples/nullish-coallescence-omittance/index.svelte
new file mode 100644
index 0000000000..a67c574fee
--- /dev/null
+++ b/packages/svelte/tests/snapshot/samples/nullish-coallescence-omittance/index.svelte
@@ -0,0 +1,8 @@
+
+Hello, {null}{name}!
+{1 ?? 'stuff'}{2 ?? 'more stuff'}{3 ?? 'even more stuff'}
+count++}>Count is {count}
+Hello, {name ?? 'earth' ?? null}
\ No newline at end of file
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..541b56a407 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
@@ -13,7 +13,7 @@ export default function Skip_static_subtree($$anchor, $$props) {
var node = $.sibling(h1, 10);
- $.html(node, () => $$props.content, false, false);
+ $.html(node, () => $$props.content);
$.next(14);
$.reset(main);
@@ -38,16 +38,12 @@ export default function Skip_static_subtree($$anchor, $$props) {
var select = $.sibling(div_1, 2);
var option = $.child(select);
- option.value = null == (option.__value = 'a') ? '' : 'a';
+ option.value = option.__value = 'a';
$.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/snapshot/samples/text-nodes-deriveds/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/text-nodes-deriveds/_expected/client/index.svelte.js
index 8e343fcf36..d520d1ef24 100644
--- a/packages/svelte/tests/snapshot/samples/text-nodes-deriveds/_expected/client/index.svelte.js
+++ b/packages/svelte/tests/snapshot/samples/text-nodes-deriveds/_expected/client/index.svelte.js
@@ -16,11 +16,9 @@ export default function Text_nodes_deriveds($$anchor) {
}
var p = root();
- const stringified_text = $.derived(() => text1() ?? '');
- const stringified_text_1 = $.derived(() => text2() ?? '');
var text = $.child(p);
- $.template_effect(() => $.set_text(text, `${$.get(stringified_text)}${$.get(stringified_text_1)}`));
$.reset(p);
+ $.template_effect(($0, $1) => $.set_text(text, `${$0 ?? ''}${$1 ?? ''}`), [text1, text2]);
$.append($$anchor, p);
}
\ No newline at end of file
diff --git a/packages/svelte/tests/snapshot/test.ts b/packages/svelte/tests/snapshot/test.ts
index f0f626f791..3a0f1f32a1 100644
--- a/packages/svelte/tests/snapshot/test.ts
+++ b/packages/svelte/tests/snapshot/test.ts
@@ -1,6 +1,6 @@
import * as fs from 'node:fs';
import { assert, expect } from 'vitest';
-import glob from 'tiny-glob/sync.js';
+import { globSync } from 'tinyglobby';
import { compile_directory } from '../helpers.js';
import { suite, type BaseTest } from '../suite.js';
import { VERSION } from 'svelte/compiler';
@@ -20,8 +20,8 @@ const { test, run } = suite(async (config, cwd) => {
fs.rmSync(`${cwd}/_expected`, { recursive: true, force: true });
fs.cpSync(`${cwd}/_output`, `${cwd}/_expected`, { recursive: true, force: true });
} else {
- const actual = glob('**', { cwd: `${cwd}/_output`, filesOnly: true });
- const expected = glob('**', { cwd: `${cwd}/_expected`, filesOnly: true });
+ const actual = globSync('**', { cwd: `${cwd}/_output`, onlyFiles: true });
+ const expected = globSync('**', { cwd: `${cwd}/_expected`, onlyFiles: true });
assert.deepEqual(actual, expected);
diff --git a/packages/svelte/tests/sourcemaps/samples/attached-sourcemap/input.svelte b/packages/svelte/tests/sourcemaps/samples/attached-sourcemap/input.svelte
index 21a47a72a9..715bbda8d9 100644
--- a/packages/svelte/tests/sourcemaps/samples/attached-sourcemap/input.svelte
+++ b/packages/svelte/tests/sourcemaps/samples/attached-sourcemap/input.svelte
@@ -8,4 +8,4 @@
replace_me_script = 'hello'
;
-{done_replace_script_2}
+{Math.random() < 1 && done_replace_script_2}
diff --git a/packages/svelte/tests/store/test.ts b/packages/svelte/tests/store/test.ts
index b23ea195d6..77cecca7e5 100644
--- a/packages/svelte/tests/store/test.ts
+++ b/packages/svelte/tests/store/test.ts
@@ -602,7 +602,7 @@ describe('toStore', () => {
assert.deepEqual(log, [0]);
set(count, 1);
- $.flush_sync();
+ $.flushSync();
assert.deepEqual(log, [0, 1]);
unsubscribe();
@@ -625,7 +625,7 @@ describe('toStore', () => {
assert.deepEqual(log, [0]);
set(count, 1);
- $.flush_sync();
+ $.flushSync();
assert.deepEqual(log, [0, 1]);
store.set(2);
@@ -654,11 +654,11 @@ describe('fromStore', () => {
assert.deepEqual(log, [0]);
store.set(1);
- $.flush_sync();
+ $.flushSync();
assert.deepEqual(log, [0, 1]);
count.current = 2;
- $.flush_sync();
+ $.flushSync();
assert.deepEqual(log, [0, 1, 2]);
assert.equal(get(store), 2);
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/a11y-no-noninteractive-element-interactions/input.svelte b/packages/svelte/tests/validator/samples/a11y-no-noninteractive-element-interactions/input.svelte
index 1adea90ba3..759f969d82 100644
--- a/packages/svelte/tests/validator/samples/a11y-no-noninteractive-element-interactions/input.svelte
+++ b/packages/svelte/tests/validator/samples/a11y-no-noninteractive-element-interactions/input.svelte
@@ -3,6 +3,7 @@
{}} on:keypress={() => {}}>
{}} on:keypress={() => {}}>
{}}>click me
+ {}}>alert
{}}>Heading
Heading
diff --git a/packages/svelte/tests/validator/samples/a11y-no-noninteractive-element-interactions/warnings.json b/packages/svelte/tests/validator/samples/a11y-no-noninteractive-element-interactions/warnings.json
index 760866d136..e8bcd0cc1a 100644
--- a/packages/svelte/tests/validator/samples/a11y-no-noninteractive-element-interactions/warnings.json
+++ b/packages/svelte/tests/validator/samples/a11y-no-noninteractive-element-interactions/warnings.json
@@ -3,60 +3,60 @@
"code": "a11y_no_noninteractive_element_interactions",
"end": {
"column": 51,
- "line": 10
+ "line": 11
},
"message": "Non-interactive element `` should not be assigned mouse or keyboard event listeners",
"start": {
"column": 0,
- "line": 10
+ "line": 11
}
},
{
"code": "a11y_no_noninteractive_element_interactions",
"end": {
"column": 58,
- "line": 11
+ "line": 12
},
"message": "Non-interactive element `
` should not be assigned mouse or keyboard event listeners",
"start": {
"column": 0,
- "line": 11
+ "line": 12
}
},
{
"code": "a11y_no_noninteractive_element_interactions",
"end": {
"column": 50,
- "line": 12
+ "line": 13
},
"message": "Non-interactive element `` should not be assigned mouse or keyboard event listeners",
"start": {
"column": 0,
- "line": 12
+ "line": 13
}
},
{
"code": "a11y_no_noninteractive_element_interactions",
"end": {
"column": 30,
- "line": 13
+ "line": 14
},
"message": "Non-interactive element ` ` should not be assigned mouse or keyboard event listeners",
"start": {
"column": 0,
- "line": 13
+ "line": 14
}
},
{
"code": "a11y_no_noninteractive_element_interactions",
"end": {
"column": 50,
- "line": 14
+ "line": 15
},
"message": "Non-interactive element `
` should not be assigned mouse or keyboard event listeners",
"start": {
"column": 0,
- "line": 14
+ "line": 15
}
}
]
diff --git a/packages/svelte/tests/validator/samples/bind-group-snippet-parameter/errors.json b/packages/svelte/tests/validator/samples/bind-group-snippet-parameter/errors.json
new file mode 100644
index 0000000000..15e762419f
--- /dev/null
+++ b/packages/svelte/tests/validator/samples/bind-group-snippet-parameter/errors.json
@@ -0,0 +1,14 @@
+[
+ {
+ "code": "bind_group_invalid_snippet_parameter",
+ "end": {
+ "column": 44,
+ "line": 2
+ },
+ "message": "Cannot `bind:group` to a snippet parameter",
+ "start": {
+ "column": 21,
+ "line": 2
+ }
+ }
+]
diff --git a/packages/svelte/tests/validator/samples/bind-group-snippet-parameter/input.svelte b/packages/svelte/tests/validator/samples/bind-group-snippet-parameter/input.svelte
new file mode 100644
index 0000000000..368484788a
--- /dev/null
+++ b/packages/svelte/tests/validator/samples/bind-group-snippet-parameter/input.svelte
@@ -0,0 +1,3 @@
+{#snippet test(group)}
+
+{/snippet}
\ No newline at end of file
diff --git a/packages/svelte/tests/validator/samples/comment-before-function-binding/errors.json b/packages/svelte/tests/validator/samples/comment-before-function-binding/errors.json
new file mode 100644
index 0000000000..fe51488c70
--- /dev/null
+++ b/packages/svelte/tests/validator/samples/comment-before-function-binding/errors.json
@@ -0,0 +1 @@
+[]
diff --git a/packages/svelte/tests/validator/samples/comment-before-function-binding/input.svelte b/packages/svelte/tests/validator/samples/comment-before-function-binding/input.svelte
new file mode 100644
index 0000000000..50200a9eac
--- /dev/null
+++ b/packages/svelte/tests/validator/samples/comment-before-function-binding/input.svelte
@@ -0,0 +1,10 @@
+
+
+
value,
+ (v) => value = v.toLowerCase()
+}
+/>
diff --git a/packages/svelte/tests/validator/samples/const-tag-inside-key-block/errors.json b/packages/svelte/tests/validator/samples/const-tag-inside-key-block/errors.json
new file mode 100644
index 0000000000..fe51488c70
--- /dev/null
+++ b/packages/svelte/tests/validator/samples/const-tag-inside-key-block/errors.json
@@ -0,0 +1 @@
+[]
diff --git a/packages/svelte/tests/validator/samples/const-tag-inside-key-block/input.svelte b/packages/svelte/tests/validator/samples/const-tag-inside-key-block/input.svelte
new file mode 100644
index 0000000000..008072bc47
--- /dev/null
+++ b/packages/svelte/tests/validator/samples/const-tag-inside-key-block/input.svelte
@@ -0,0 +1,3 @@
+{#key 'key'}
+ {@const foo = 'bar'}
+{/key}
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/const-tag-placement-1/errors.json b/packages/svelte/tests/validator/samples/const-tag-placement-1/errors.json
index 144345527a..514e5d0561 100644
--- a/packages/svelte/tests/validator/samples/const-tag-placement-1/errors.json
+++ b/packages/svelte/tests/validator/samples/const-tag-placement-1/errors.json
@@ -1,7 +1,7 @@
[
{
"code": "const_tag_invalid_placement",
- "message": "`{@const}` must be the immediate child of `{#snippet}`, `{#if}`, `{:else if}`, `{:else}`, `{#each}`, `{:then}`, `{:catch}`, `
` or ``",
+ "message": "`{@const}` must be the immediate child of `{#snippet}`, `{#if}`, `{:else if}`, `{:else}`, `{#each}`, `{:then}`, `{:catch}`, ``, ``",
"start": {
"line": 5,
"column": 0
diff --git a/packages/svelte/tests/validator/samples/const-tag-placement-2/errors.json b/packages/svelte/tests/validator/samples/const-tag-placement-2/errors.json
index 64a24c5f48..6b968f7eda 100644
--- a/packages/svelte/tests/validator/samples/const-tag-placement-2/errors.json
+++ b/packages/svelte/tests/validator/samples/const-tag-placement-2/errors.json
@@ -1,7 +1,7 @@
[
{
"code": "const_tag_invalid_placement",
- "message": "`{@const}` must be the immediate child of `{#snippet}`, `{#if}`, `{:else if}`, `{:else}`, `{#each}`, `{:then}`, `{:catch}`, `` or ``",
+ "message": "`{@const}` must be the immediate child of `{#snippet}`, `{#if}`, `{:else if}`, `{:else}`, `{#each}`, `{:then}`, `{:catch}`, ``, ``",
"start": {
"line": 7,
"column": 4
diff --git a/packages/svelte/tests/validator/samples/const-tag-placement-svelte-boundary/errors.json b/packages/svelte/tests/validator/samples/const-tag-placement-svelte-boundary/errors.json
new file mode 100644
index 0000000000..fe51488c70
--- /dev/null
+++ b/packages/svelte/tests/validator/samples/const-tag-placement-svelte-boundary/errors.json
@@ -0,0 +1 @@
+[]
diff --git a/packages/svelte/tests/validator/samples/const-tag-placement-svelte-boundary/input.svelte b/packages/svelte/tests/validator/samples/const-tag-placement-svelte-boundary/input.svelte
new file mode 100644
index 0000000000..5708cc36ca
--- /dev/null
+++ b/packages/svelte/tests/validator/samples/const-tag-placement-svelte-boundary/input.svelte
@@ -0,0 +1,11 @@
+
+
+
+ {@const x = a}
+ {#snippet failed()}
+ {x}
+ {/snippet}
+