diff --git a/.changeset/fuzzy-zoos-repeat.md b/.changeset/fuzzy-zoos-repeat.md deleted file mode 100644 index 3fb3f0502e..0000000000 --- a/.changeset/fuzzy-zoos-repeat.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: value/checked not correctly set using spread diff --git a/.changeset/hip-singers-vanish.md b/.changeset/hip-singers-vanish.md deleted file mode 100644 index 9dce4d98a8..0000000000 --- a/.changeset/hip-singers-vanish.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': minor ---- - -feat: SSR-safe ID generation with `$props.id()` diff --git a/.changeset/slow-meals-wait.md b/.changeset/slow-meals-wait.md deleted file mode 100644 index e1408e3849..0000000000 --- a/.changeset/slow-meals-wait.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: use `importNode` to clone templates for Firefox diff --git a/.changeset/thick-carrots-arrive.md b/.changeset/thick-carrots-arrive.md deleted file mode 100644 index 582cf5e6e1..0000000000 --- a/.changeset/thick-carrots-arrive.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: recurse into `$derived` for ownership validation 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/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 99f8153517..90d219faae 100644 --- a/.github/workflows/pkg.pr.new.yml +++ b/.github/workflows/pkg.pr.new.yml @@ -3,6 +3,8 @@ on: [push, pull_request] jobs: build: + permissions: {} + runs-on: ubuntu-latest steps: 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/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 7d2f718da7..7ea7164752 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ -[![license](https://img.shields.io/npm/l/svelte.svg)](LICENSE.md) [![Chat](https://img.shields.io/discord/457912077277855764?label=chat&logo=discord)](https://svelte.dev/chat) +[![License](https://img.shields.io/npm/l/svelte.svg)](LICENSE.md) [![Chat](https://img.shields.io/discord/457912077277855764?label=chat&logo=discord)](https://svelte.dev/chat) ## What is Svelte? 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/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/04-$effect.md b/documentation/docs/02-runes/04-$effect.md index da24084d4d..e346bceba8 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 - + ``` Components also support `bind:this`, allowing you to interact with component instances programmatically. diff --git a/documentation/docs/06-runtime/03-lifecycle-hooks.md b/documentation/docs/06-runtime/03-lifecycle-hooks.md index 2b97ca796f..f051c46d73 100644 --- a/documentation/docs/06-runtime/03-lifecycle-hooks.md +++ b/documentation/docs/06-runtime/03-lifecycle-hooks.md @@ -147,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 94ade6e887..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 @@ -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. diff --git a/documentation/docs/07-misc/99-faq.md b/documentation/docs/07-misc/99-faq.md index b56c27af86..7e25cdab55 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
``` --> diff --git a/documentation/docs/98-reference/.generated/client-errors.md b/documentation/docs/98-reference/.generated/client-errors.md index 2c2e0707ea..0beb3cb9a9 100644 --- a/documentation/docs/98-reference/.generated/client-errors.md +++ b/documentation/docs/98-reference/.generated/client-errors.md @@ -133,3 +133,27 @@ Reading state that was created inside the same derived is forbidden. Consider us ``` Updating state inside a derived or a template expression is forbidden. If the value should not be reactive, declare it without `$state` ``` + +This error is thrown in a situation like this: + +```svelte + + + +``` + +Here, the `$derived` updates `count`, which is `$state` and therefore forbidden to do. It is forbidden because the reactive graph could become unstable as a result, leading to subtle bugs, like values being stale or effects firing in the wrong order. To prevent this, Svelte errors when detecting an update to a `$state` variable. + +To fix this: +- See if it's possible to refactor your `$derived` such that the update becomes unnecessary +- Think about why you need to update `$state` inside a `$derived` in the first place. Maybe it's because you're using `bind:`, which leads you down a bad code path, and separating input and output path (by splitting it up to an attribute and an event, or by using [Function bindings](bind#Function-bindings)) makes it possible avoid the update +- If it's unavoidable, you may need to use an [`$effect`]($effect) instead. This could include splitting parts of the `$derived` into an [`$effect`]($effect) which does the updates diff --git a/documentation/docs/98-reference/.generated/compile-errors.md b/documentation/docs/98-reference/.generated/compile-errors.md index 7eb53cff93..c2d4b28159 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 ``` diff --git a/package.json b/package.json index 2fe545b361..ad69bfc9ca 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ "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.1.9" } diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index e112bf6209..e10a606fe4 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,170 @@ # svelte +## 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 diff --git a/packages/svelte/elements.d.ts b/packages/svelte/elements.d.ts index 6d256b5620..99d87b4c09 100644 --- a/packages/svelte/elements.d.ts +++ b/packages/svelte/elements.d.ts @@ -957,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 { @@ -1075,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?: @@ -1086,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; diff --git a/packages/svelte/messages/client-errors/errors.md b/packages/svelte/messages/client-errors/errors.md index ce1f222c63..ab4d1519c1 100644 --- a/packages/svelte/messages/client-errors/errors.md +++ b/packages/svelte/messages/client-errors/errors.md @@ -87,3 +87,27 @@ See the [migration guide](/docs/svelte/v5-migration-guide#Components-are-no-long ## 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 is thrown in a situation like this: + +```svelte + + + +``` + +Here, the `$derived` updates `count`, which is `$state` and therefore forbidden to do. It is forbidden because the reactive graph could become unstable as a result, leading to subtle bugs, like values being stale or effects firing in the wrong order. To prevent this, Svelte errors when detecting an update to a `$state` variable. + +To fix this: +- See if it's possible to refactor your `$derived` such that the update becomes unnecessary +- Think about why you need to update `$state` inside a `$derived` in the first place. Maybe it's because you're using `bind:`, which leads you down a bad code path, and separating input and output path (by splitting it up to an attribute and an event, or by using [Function bindings](bind#Function-bindings)) makes it possible avoid the update +- If it's unavoidable, you may need to use an [`$effect`]($effect) instead. This could include splitting parts of the `$derived` into an [`$effect`]($effect) which does the updates diff --git a/packages/svelte/messages/compile-errors/template.md b/packages/svelte/messages/compile-errors/template.md index d061416dbd..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 diff --git a/packages/svelte/package.json b/packages/svelte/package.json index a4594b2a5c..6f10b2a9ea 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.19.10", + "version": "5.23.1", "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", @@ -137,11 +138,11 @@ "@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.1.9" }, @@ -150,7 +151,7 @@ "@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", diff --git a/packages/svelte/src/compiler/errors.js b/packages/svelte/src/compiler/errors.js index 634faf15a2..161b578203 100644 --- a/packages/svelte/src/compiler/errors.js +++ b/packages/svelte/src/compiler/errors.js @@ -762,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 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/style.js b/packages/svelte/src/compiler/phases/1-parse/read/style.js index 29e8a0e541..f8c39f1b1d 100644 --- a/packages/svelte/src/compiler/phases/1-parse/read/style.js +++ b/packages/svelte/src/compiler/phases/1-parse/read/style.js @@ -566,7 +566,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 +574,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..09eb0bfa68 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 @@ -118,6 +118,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/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..362ac9dcad 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 @@ -133,7 +133,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') || 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 fc8108e46e..070ec7cd34 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 @@ -5,9 +5,12 @@ import { regex_ends_with_whitespace, regex_starts_with_whitespace } from '../../ 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 +46,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 +96,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 +184,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 +209,63 @@ 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; +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; - const name = relative_selector.combinator.name; - - 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') { // `{@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))) ); } @@ -313,9 +326,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,13 +345,6 @@ 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:has(.scoped), :global(.foo):has(.scoped), or :root { &:has(.scoped) {} } @@ -353,46 +360,6 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element) ) ) ); - 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 @@ -403,37 +370,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) { @@ -458,7 +422,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 @@ -507,7 +471,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')) { @@ -591,7 +555,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; @@ -612,80 +576,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 containing the element in question - // skipping nodes that appears before the element - - const seen = new Set(); - let skip = true; - - /** @param {Compiler.AST.SvelteNode} node */ - function get_siblings(node) { - walk(node, null, { - RegularElement(node) { - if (node === element) { - skip = false; - if (include_self) siblings.push(node); - } else if (!skip) { - siblings.push(node); - } - }, - SvelteElement(node) { - if (node === element) { - skip = false; - if (include_self) siblings.push(node); - } else if (!skip) { - siblings.push(node); - } - }, - RenderTag(node) { - for (const snippet of node.metadata.snippets) { - if (seen.has(snippet)) continue; - - seen.add(snippet); - get_siblings(snippet.body); - } - } - }); - } - - for (const node of nodes.slice(nodes.indexOf(start))) { - get_siblings(node); - } - - return siblings; -} - /** * @param {any} operator * @param {any} expected_value @@ -822,6 +712,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} @@ -843,11 +811,12 @@ 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} */ -function get_possible_element_siblings(node, adjacent_only, seen = new Set()) { +function get_possible_element_siblings(node, direction, adjacent_only, seen = new Set()) { /** @type {Map} */ const result = new Map(); const path = node.metadata.path; @@ -859,9 +828,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') { @@ -876,21 +845,28 @@ function get_possible_element_siblings(node, adjacent_only, seen = new Set()) { return result; } } + // 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)) { if (node.type === 'SlotElement') { 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)) { 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]; @@ -910,7 +886,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)) { @@ -923,7 +899,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); } } @@ -931,11 +907,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} node + * @param {Direction} direction * @param {boolean} adjacent_only + * @param {Set} seen * @returns {Map} */ -function get_possible_last_child(node, adjacent_only) { +function get_possible_nested_siblings(node, direction, adjacent_only, seen = new Set()) { /** @type {Array} */ let fragments = []; @@ -956,12 +934,20 @@ 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; } /** @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) { @@ -969,7 +955,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); @@ -1012,27 +998,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) { +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') { @@ -1042,13 +1029,19 @@ function loop_child(children, adjacent_only) { } } 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 846abcf7df..1f636c32df 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -258,8 +258,18 @@ export function analyze_module(ast, options) { { 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 ); @@ -554,7 +564,7 @@ export function analyze_component(root, source, options) { binding.declaration_kind !== 'import' ) { binding.kind = 'state'; - binding.mutated = binding.updated = true; + binding.mutated = true; } } } @@ -608,9 +618,7 @@ export function analyze_component(root, source, options) { expression: null, 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); @@ -672,9 +680,7 @@ 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, derived_state: [], @@ -755,66 +761,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]; - } + // 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 } - } 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); - } - } - } + ]) + ); } } 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 14b14f9c84..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,13 +18,11 @@ export interface AnalysisState { component_slots: Set; /** Information about the current expression/directive/block value */ expression: ExpressionMetadata | null; - 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 42e4498969..9124a8822f 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/Attribute.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/Attribute.js @@ -162,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; } } 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 7719eee677..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'; @@ -158,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); @@ -184,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') { @@ -199,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 b6aebc9002..022489555c 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js @@ -42,6 +42,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': @@ -218,14 +221,6 @@ export function CallExpression(node, context) { break; } - 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 ed397258f8..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,7 +7,7 @@ import { get_rune } from '../../scope.js'; * @param {Context} context */ export function ClassBody(node, context) { - /** @type {string[]} */ + /** @type {{name: string, private: boolean}[]} */ const derived_state = []; for (const definition of node.body) { @@ -18,7 +18,10 @@ export function ClassBody(node, context) { ) { const rune = get_rune(definition.value, context.state.scope); if (rune === '$derived' || rune === '$derived.by') { - derived_state.push(definition.key.name); + derived_state.push({ + name: definition.key.name, + private: definition.key.type === 'PrivateIdentifier' + }); } } } 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 3f5e0473c5..f723f8447c 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/ConstTag.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/ConstTag.js @@ -25,6 +25,7 @@ export function ConstTag(node, context) { 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/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/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/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 24a8e5122d..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( 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 2d90c85364..04f4347a40 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, Identifier, Literal, Node, Pattern, PrivateIdentifier, Super, UpdateExpression, VariableDeclarator } from 'estree' */ /** @import { AST, Binding } from '#compiler' */ /** @import { AnalysisState, Context } from '../../types' */ /** @import { Scope } from '../../../scope' */ @@ -10,12 +10,12 @@ import * as b from '../../../../utils/builders.js'; 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); @@ -38,16 +38,22 @@ export function validate_assignment(node, argument, state) { e.snippet_parameter_assignment(node); } } - if ( argument.type === 'MemberExpression' && argument.object.type === 'ThisExpression' && (((argument.property.type === 'PrivateIdentifier' || argument.property.type === 'Identifier') && - state.derived_state.includes(argument.property.name)) || + state.derived_state.some( + (derived) => + derived.name === /** @type {PrivateIdentifier | Identifier} */ (argument.property).name && + derived.private === (argument.property.type === 'PrivateIdentifier') + )) || (argument.property.type === 'Literal' && argument.property.value && typeof argument.property.value === 'string' && - state.derived_state.includes(argument.property.value))) + state.derived_state.some( + (derived) => + derived.name === /** @type {Literal} */ (argument.property).value && !derived.private + ))) ) { e.constant_assignment(node, 'derived state'); } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js index 2b8a3f2945..a93be76872 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 @@ -315,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) { @@ -599,7 +599,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 f4ac6c1bb4..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 @@ -45,6 +45,7 @@ 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 init: Statement[]; 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..2e051ec674 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 @@ -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 4d34f2c468..916c6aca30 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 @@ -119,6 +119,7 @@ function build_assignment(operator, left, right, context) { 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) 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 e0aef2d316..7588b24280 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 @@ -60,7 +60,7 @@ 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 ) 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..c563920855 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 @@ -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/CallExpression.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/CallExpression.js index 7a3057451a..fda43ad791 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 @@ -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/EachBlock.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js index 24a696e7d2..629cacda01 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 @@ -143,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 */ 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 e9cfd9c506..389a694741 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Fragment.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Fragment.js @@ -48,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 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..fdd21b2b7e 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,4 +1,4 @@ -/** @import { BlockStatement, Expression } from 'estree' */ +/** @import { BlockStatement, Expression, Identifier } from 'estree' */ /** @import { AST } from '#compiler' */ /** @import { ComponentContext } from '../types' */ import * as b from '../../../../utils/builders.js'; @@ -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/RegularElement.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js index 98036aa9b6..45a594af1f 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, Statement } from 'estree' */ +/** @import { ArrayExpression, Expression, ExpressionStatement, Identifier, MemberExpression, ObjectExpression, Statement } from 'estree' */ /** @import { AST } from '#compiler' */ /** @import { SourceLocation } from '#shared' */ /** @import { ComponentClientTransformState, ComponentContext } from '../types' */ @@ -20,16 +20,17 @@ 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_assignment, - get_expression_id + 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, - is_custom_element_node(node) && 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,20 +294,30 @@ export function RegularElement(node, context) { }` ); } - continue; - } + } 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) + ); - 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; + const update = build_element_attribute_update(node, node_id, name, value, attributes); + + (has_state ? context.state.update : context.state.init).push(b.stmt(update)); + } } } - // 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); - if ( is_load_error_element(node.name) && (has_spread || has_use || lookup.has('onload') || lookup.has('onerror')) @@ -363,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; @@ -491,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. @@ -515,63 +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 { value, has_state } = build_attribute_value(attribute.value, context, (value, metadata) => - metadata.has_call ? get_expression_id(state, value) : value - ); - - if (name === 'autofocus') { - state.init.push(b.stmt(b.call('$.autofocus', node_id, value))); - return false; +function build_element_attribute_update(element, node_id, name, value, attributes) { + if (name === 'muted') { + // 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); } - // Special case for Firefox who needs it set as a property in order to work - if (name === 'muted') { - state.init.push(b.stmt(b.assignment('=', b.member(node_id, b.id('muted')), value))); - return false; + if (name === 'value') { + return b.call('$.set_value', node_id, value); } - /** @type {Statement} */ - let update; + if (name === 'checked') { + return b.call('$.set_checked', node_id, value); + } - if (name === 'class') { - if (attribute.metadata.needs_clsx) { - value = b.call('$.clsx', value); - } + 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 @@ -582,70 +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 { - 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 (has_state) { - 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 { value, has_state } = 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 (has_state) { - // this is different from other updates — it doesn't get grouped, - // because set_custom_element_data may not be idempotent - state.init.push(b.stmt(b.call('$.template_effect', b.thunk(update.expression)))); - return true; - } else { - state.init.push(update); - return false; - } + context.state.init.push(b.stmt(update)); } /** @@ -656,29 +668,33 @@ 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 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 ? get_expression_id(state, value) : value + 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 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.binary('==', b.null, b.assignment('=', b.member(node_id, '__value'), value)), b.literal(''), // render null/undefined values as empty string to support placeholder options value ) ); - 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([ @@ -708,9 +724,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/SlotElement.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SlotElement.js index fdd705e32e..c6f4ba1ed3 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 @@ -59,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/SvelteBoundary.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteBoundary.js index 6da650a591..9228df9703 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,6 +1,7 @@ /** @import { BlockStatement, Statement, Expression } from 'estree' */ /** @import { AST } from '#compiler' */ /** @import { ComponentContext } from '../types' */ +import { dev } from '../../../../state.js'; import * as b from '../../../../utils/builders.js'; /** @@ -35,6 +36,9 @@ export function SvelteBoundary(node, context) { /** @type {Statement[]} */ const external_statements = []; + /** @type {Statement[]} */ + const internal_statements = []; + const snippets_visits = []; // Capture the `failed` implicit snippet prop @@ -53,7 +57,20 @@ export function SvelteBoundary(node, context) { /** @type {Statement[]} */ const init = []; context.visit(child, { ...context.state, init }); - external_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); } @@ -63,6 +80,10 @@ export function SvelteBoundary(node, context) { 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)) ); 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 e275283655..115eb6ccc1 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 @@ -5,13 +5,8 @@ import { dev, locator } from '../../../../state.js'; import { is_text_attribute } from '../../../../utils/ast.js'; import * as b from '../../../../utils/builders.js'; 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 } from './shared/utils.js'; +import { build_attribute_value, build_set_attributes, build_set_class } from './shared/element.js'; +import { build_render_statement, get_expression_id } from './shared/utils.js'; /** * @param {AST.SvelteElement} node @@ -77,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) { 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 d636e42cde..5e3c329dbf 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 @@ -116,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; let options = args.length === 2 ? /** @type {Expression} */ (context.visit(args[1])) : undefined; 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 abffad0ff7..97cec7a729 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,32 +1,31 @@ -/** @import { Expression, Identifier, ObjectExpression } from 'estree' */ +/** @import { ArrayExpression, Expression, Identifier, ObjectExpression } from 'estree' */ /** @import { AST, ExpressionMetadata } from '#compiler' */ /** @import { ComponentClientTransformState, ComponentContext } from '../../types' */ +import { escape_html } from '../../../../../../escaping.js'; import { normalize_attribute } from '../../../../../../utils.js'; import { is_ignored } from '../../../../../state.js'; import { is_event_attribute } from '../../../../../utils/ast.js'; import * as b from '../../../../../utils/builders.js'; -import { build_getter } from '../../utils.js'; +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 is_dynamic = false; @@ -68,14 +67,39 @@ export function build_set_attributes( } } + 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, - is_dynamic ? 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 ); @@ -83,87 +107,8 @@ export function build_set_attributes( context.state.init.push(b.let(attributes_id)); const update = b.stmt(b.assignment('=', attributes_id, call)); context.state.update.push(update); - return true; - } - - 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 {ComponentContext} context - * @param {boolean} is_attributes_reactive - */ -export function build_style_directives( - style_directives, - element_id, - context, - is_attributes_reactive -) { - const state = context.state; - - for (const directive of style_directives) { - const { has_state } = directive.metadata.expression; - - let value = - 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 update = b.stmt( - b.call( - '$.set_style', - element_id, - b.literal(directive.name), - value, - /** @type {Expression} */ (directive.modifiers.includes('important') ? b.true : undefined) - ) - ); - - if (has_state || is_attributes_reactive) { - state.update.push(update); - } else { - state.init.push(update); - } - } -} - -/** - * 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.ClassDirective[]} class_directives - * @param {Identifier} element_id - * @param {ComponentContext} context - * @param {boolean} is_attributes_reactive - */ -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)); - - if (has_call) { - value = get_expression_id(state, value); - } - - const update = b.stmt(b.call('$.toggle_class', element_id, b.literal(directive.name), value)); - - if (is_attributes_reactive || has_state) { - state.update.push(update); - } else { - state.init.push(update); - } + } else { + context.state.init.push(b.stmt(call)); } } @@ -175,7 +120,7 @@ export function build_class_directives( */ export function build_attribute_value(value, context, memoize = (value) => value) { if (value === true) { - return { value: b.literal(true), has_state: false }; + return { value: b.true, has_state: false }; } if (!Array.isArray(value) || value.length === 1) { @@ -207,3 +152,120 @@ export function get_attribute_name(element, attribute) { return attribute.name; } + +/** + * @param {AST.RegularElement | AST.SvelteElement} element + * @param {Identifier} node_id + * @param {AST.Attribute} attribute + * @param {AST.ClassDirective[]} class_directives + * @param {ComponentContext} context + * @param {boolean} is_html + */ +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); + } + + return metadata.has_call ? get_expression_id(context.state, value) : value; + }); + + /** @type {Identifier | undefined} */ + let previous_id; + + /** @type {ObjectExpression | Identifier | undefined} */ + let prev; + + /** @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 { + prev = b.object([]); + } + } + + /** @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 (!css_hash && next) { + css_hash = b.null; + } + + /** @type {Expression} */ + let set_class = b.call( + '$.set_class', + node_id, + is_html ? b.literal(1) : b.literal(0), + value, + css_hash, + prev, + next + ); + + if (previous_id) { + set_class = b.assignment('=', previous_id, set_class); + } + + (has_state ? context.state.update : context.state.init).push(b.stmt(set_class)); +} + +/** + * @param {Identifier} node_id + * @param {AST.Attribute} attribute + * @param {AST.StyleDirective[]} style_directives + * @param {ComponentContext} context + */ +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([]); + } + } + + /** @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..2667a96f6a 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 @@ -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 5bc3041ca4..f076d7c11e 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 @@ -144,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/utils.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js index 9214a13c94..df6308d631 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 @@ -26,55 +26,9 @@ export function memoize_expression(state, value) { * @param {Expression} value */ export function get_expression_id(state, value) { - for (let i = 0; i < state.expressions.length; i += 1) { - if (compare_expressions(state.expressions[i], value)) { - return b.id(`$${i}`); - } - } - return b.id(`$${state.expressions.push(value) - 1}`); } -/** - * Returns true of two expressions have an identical AST shape - * @param {Expression} a - * @param {Expression} b - */ -function compare_expressions(a, b) { - if (a.type !== b.type) { - return false; - } - - for (const key in a) { - if (key === 'type' || key === 'metadata' || key === 'loc' || key === 'start' || key === 'end') { - continue; - } - - const va = /** @type {any} */ (a)[key]; - const vb = /** @type {any} */ (b)[key]; - - if ((typeof va === 'object') !== (typeof vb === 'object')) { - return false; - } - - if (typeof va !== 'object' || va === null || vb === null) { - if (va !== vb) return false; - } else if (Array.isArray(va)) { - if (va.length !== vb.length) { - return false; - } - - if (va.some((v, i) => !compare_expressions(v, vb[i]))) { - return false; - } - } else if (!compare_expressions(va, vb)) { - return false; - } - } - - return true; -} - /** * @param {Array} values * @param {(node: AST.SvelteNode, state: any) => any} visit @@ -101,11 +55,15 @@ export function build_template_chunk( 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 { + } 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 @@ -117,31 +75,33 @@ export function build_template_chunk( // If we have a single expression, then pass that in directly to possibly avoid doing // extra work in the template_effect (instead we do the work in set_text). return { value, has_state }; - } else { - // add `?? ''` where necessary (TODO optimise more cases) - if ( - value.type === 'LogicalExpression' && - value.right.type === 'Literal' && - (value.operator === '??' || value.operator === '||') - ) { - // `foo ?? null` -=> `foo ?? ''` - // otherwise leave the expression untouched - if (value.right.value === null) { - value = { ...value, right: b.literal('') }; - } - } else if ( - state.analysis.props_id && - value.type === 'Identifier' && - value.name === state.analysis.props_id.name - ) { - // do nothing ($props.id() is never null/undefined) - } else { - value = b.logical('??', value, b.literal('')); + } + + 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('') }; } + } + + const is_defined = + value.type === 'BinaryExpression' || + (value.type === 'UnaryExpression' && value.operator !== 'void') || + (value.type === 'LogicalExpression' && value.right.type === 'Literal') || + (value.type === 'Identifier' && value.name === state.analysis.props_id?.name); - expressions.push(value); + if (!is_defined) { + // add `?? ''` where necessary (TODO optimise more cases) + value = b.logical('??', value, b.literal('')); } + expressions.push(value); + quasi = b.quasi('', i + 1 === values.length); quasis.push(quasi); } @@ -151,7 +111,10 @@ export function build_template_chunk( quasi.value.raw = sanitize_template_string(/** @type {string} */ (quasi.value.cooked)); } - const value = b.template(quasis, expressions); + const value = + expressions.length > 0 + ? b.template(quasis, expressions) + : b.literal(/** @type {string} */ (quasi.value.cooked)); return { value, has_state }; } @@ -309,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 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..a425bc5ec4 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 @@ -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/IfBlock.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/IfBlock.js index 4df09aa8b9..cbdd2cd8cc 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 @@ -1,4 +1,4 @@ -/** @import { BlockStatement, Expression } from 'estree' */ +/** @import { BlockStatement, Expression, IfStatement } from 'estree' */ /** @import { AST } from '#compiler' */ /** @import { ComponentContext } from '../types.js' */ import { BLOCK_OPEN_ELSE } from '../../../../../internal/server/hydration.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/SlotElement.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/SlotElement.js index 7ece04ae3d..e7925071cd 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 @@ -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/VariableDeclaration.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/VariableDeclaration.js index c4c31d7eb3..a9c9777335 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 @@ -45,7 +45,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); } } @@ -75,8 +75,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 === '$derived.by') { declarations.push( 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..4a5becfb2f 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,4 +1,4 @@ -/** @import { Expression, Literal } from 'estree' */ +/** @import { ArrayExpression, Expression, Literal, ObjectExpression } from 'estree' */ /** @import { AST, Namespace } from '#compiler' */ /** @import { ComponentContext, ComponentServerTransformState } from '../../types.js' */ import { @@ -24,6 +24,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 +48,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 +83,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 +204,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 +239,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 +309,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 +318,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 +356,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/scope.js b/packages/svelte/src/compiler/phases/scope.js index 51e9eb088d..b6063c3234 100644 --- a/packages/svelte/src/compiler/phases/scope.js +++ b/packages/svelte/src/compiler/phases/scope.js @@ -1,6 +1,6 @@ -/** @import { ClassDeclaration, Expression, FunctionDeclaration, Identifier, ImportDeclaration, MemberExpression, Node, Pattern, VariableDeclarator } from 'estree' */ +/** @import { ArrowFunctionExpression, ClassDeclaration, Expression, FunctionDeclaration, FunctionExpression, Identifier, ImportDeclaration, MemberExpression, Node, Pattern, 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'; @@ -16,6 +16,90 @@ 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'; +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; + + /** @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' + ); + } +} + export class Scope { /** @type {ScopeRoot} */ root; @@ -96,26 +180,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); @@ -690,8 +763,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/types/index.d.ts b/packages/svelte/src/compiler/types/index.d.ts index b80b717e42..eec41bad9d 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'; @@ -241,6 +234,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 +258,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 +269,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/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/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/index-client.js b/packages/svelte/src/index-client.js index ca29d5bfbe..fd8e999da7 100644 --- a/packages/svelte/src/index-client.js +++ b/packages/svelte/src/index-client.js @@ -1,7 +1,7 @@ /** @import { ComponentContext, ComponentContextLegacy } from '#client' */ /** @import { EventDispatcher } from './index.js' */ /** @import { NotFunction } from './internal/types.js' */ -import { 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'; @@ -45,13 +45,14 @@ if (DEV) { } /** - * 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 @@ -206,15 +207,7 @@ 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 { tick, untrack } from './internal/client/runtime.js'; diff --git a/packages/svelte/src/index.d.ts b/packages/svelte/src/index.d.ts index f94e6b5425..594c063cfe 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/context.js b/packages/svelte/src/internal/client/context.js index bd94d5ad8a..bfca9d5e6a 100644 --- a/packages/svelte/src/internal/client/context.js +++ b/packages/svelte/src/internal/client/context.js @@ -11,7 +11,7 @@ import { set_active_reaction, untrack } from './runtime.js'; -import { effect } from './reactivity/effects.js'; +import { effect, teardown } from './reactivity/effects.js'; import { legacy_mode_flag } from '../flags/index.js'; /** @type {ComponentContext | null} */ @@ -112,15 +112,16 @@ export function getAllContexts() { * @returns {void} */ export function push(props, runes = false, fn) { - component_context = { + 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 = { @@ -131,6 +132,10 @@ export function push(props, runes = false, fn) { }; } + teardown(() => { + /** @type {ComponentContext} */ (ctx).d = true; + }); + if (DEV) { // component function component_context.function = fn; diff --git a/packages/svelte/src/internal/client/dom/blocks/await.js b/packages/svelte/src/internal/client/dom/blocks/await.js index c8c7c1c0ea..2e3d229779 100644 --- a/packages/svelte/src/internal/client/dom/blocks/await.js +++ b/packages/svelte/src/internal/client/dom/blocks/await.js @@ -3,7 +3,7 @@ import { DEV } from 'esm-env'; import { is_promise } from '../../../shared/utils.js'; import { block, branch, pause_effect, resume_effect } from '../../reactivity/effects.js'; import { internal_set, mutable_source, source } from '../../reactivity/sources.js'; -import { flush_sync, set_active_effect, set_active_reaction } from '../../runtime.js'; +import { flushSync, set_active_effect, set_active_reaction } from '../../runtime.js'; import { hydrate_next, hydrate_node, hydrating } from '../hydration.js'; import { queue_micro_task } from '../task.js'; import { UNINITIALIZED } from '../../../../constants.js'; @@ -105,7 +105,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(); } } } diff --git a/packages/svelte/src/internal/client/dom/blocks/if.js b/packages/svelte/src/internal/client/dom/blocks/if.js index 36790c05c1..423c436fe4 100644 --- a/packages/svelte/src/internal/client/dom/blocks/if.js +++ b/packages/svelte/src/internal/client/dom/blocks/if.js @@ -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/elements/attributes.js b/packages/svelte/src/internal/client/dom/elements/attributes.js index 2dba2d797a..5a5d5d7c9b 100644 --- a/packages/svelte/src/internal/client/dom/elements/attributes.js +++ b/packages/svelte/src/internal/client/dom/elements/attributes.js @@ -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 === @@ -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; @@ -213,6 +214,7 @@ export function set_custom_element_data(node, prop, value) { // or effect var previous_reaction = active_reaction; var previous_effect = active_effect; + // If we're hydrating but the custom element is from Svelte, and it already scaffolded, // then it might run block logic in hydration mode, which we have to prevent. let was_hydrating = hydrating; @@ -222,17 +224,20 @@ export function set_custom_element_data(node, prop, value) { 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; @@ -254,23 +259,18 @@ export function set_custom_element_data(node, prop, value) { /** * 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; @@ -289,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 @@ -324,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; @@ -375,8 +389,9 @@ 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 (!is_custom_element && (key === '__value' || (key === 'value' && value != null))) { @@ -422,13 +437,9 @@ export function set_attributes( // @ts-ignore element[name] = value; } else if (typeof value !== 'function') { - 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) { @@ -438,6 +449,20 @@ export function set_attributes( 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(); diff --git a/packages/svelte/src/internal/client/dom/elements/class.js b/packages/svelte/src/internal/client/dom/elements/class.js index 62ffb6d14b..ecbfcbc010 100644 --- a/packages/svelte/src/internal/client/dom/elements/class.js +++ b/packages/svelte/src/internal/client/dom/elements/class.js @@ -1,120 +1,47 @@ +import { to_class } from '../../../shared/attributes.js'; import { hydrating } from '../hydration.js'; /** - * @param {SVGElement} dom - * @param {string} value - * @param {string} [hash] - * @returns {void} - */ -export function set_svg_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); - } - - // @ts-expect-error need to add __className to patched prototype - dom.__className = next_class_name; - } -} - -/** - * @param {MathMLElement} 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_mathml_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); - - 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); + var prev = dom.__className; + + if (hydrating || prev !== value) { + 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 {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; + 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; } -} -/** - * @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/events.js b/packages/svelte/src/internal/client/dom/elements/events.js index 363b8e1ed5..0c1bb1dada 100644 --- a/packages/svelte/src/internal/client/dom/elements/events.js +++ b/packages/svelte/src/internal/client/dom/elements/events.js @@ -237,7 +237,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 +311,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 +325,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..fbc1da95df 100644 --- a/packages/svelte/src/internal/client/dom/elements/transitions.js +++ b/packages/svelte/src/internal/client/dom/elements/transitions.js @@ -14,6 +14,7 @@ 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 { 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/operations.js b/packages/svelte/src/internal/client/dom/operations.js index 83565d17ae..0ad9045b20 100644 --- a/packages/svelte/src/internal/client/dom/operations.js +++ b/packages/svelte/src/internal/client/dom/operations.js @@ -44,11 +44,11 @@ export function init_operations() { // @ts-expect-error element_prototype.__click = undefined; // @ts-expect-error - element_prototype.__className = ''; + element_prototype.__className = undefined; // @ts-expect-error element_prototype.__attributes = null; // @ts-expect-error - element_prototype.__styles = null; + element_prototype.__style = undefined; // @ts-expect-error element_prototype.__e = undefined; 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 6ff3b0fa19..de2df62c92 100644 --- a/packages/svelte/src/internal/client/dom/template.js +++ b/packages/svelte/src/internal/client/dom/template.js @@ -250,8 +250,6 @@ export function append(anchor, dom) { anchor.before(/** @type {Node} */ (dom)); } -let uid = 1; - /** * Create (or hydrate) an unique UID for the component instance. */ @@ -260,12 +258,16 @@ export function props_id() { hydrating && hydrate_node && hydrate_node.nodeType === 8 && - hydrate_node.textContent?.startsWith('#s') + hydrate_node.textContent?.startsWith(`#`) ) { const id = hydrate_node.textContent.substring(1); hydrate_next(); return id; } - return 'c' + uid++; + // @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/index.js b/packages/svelte/src/internal/client/index.js index e69f3c07d5..9b95d5eebc 100644 --- a/packages/svelte/src/internal/client/index.js +++ b/packages/svelte/src/internal/client/index.js @@ -39,9 +39,11 @@ export { 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'; @@ -146,7 +148,7 @@ export { get, safe_get, invalidate_inner_signals, - flush_sync, + flushSync as flush, tick, untrack, exclude_from_object, diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index 59a7ed0f16..795417cc0f 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -116,7 +116,7 @@ function get_derived_parent_effect(derived) { * @param {Derived} derived * @returns {T} */ -export function execute_derived(derived) { +function execute_derived(derived) { var value; var prev_active_effect = active_effect; diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index 9d7b5e9de6..468bb94ab4 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -6,15 +6,12 @@ import { 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 { @@ -85,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; } } @@ -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); @@ -144,9 +135,9 @@ function create_effect(type, fn, sync, push = true) { effect.teardown === null && (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 @@ -399,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; } } diff --git a/packages/svelte/src/internal/client/reactivity/props.js b/packages/svelte/src/internal/client/reactivity/props.js index 5a3b30281f..bd85b14df0 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, @@ -10,24 +10,10 @@ import { import { get_descriptor, is_function } from '../../shared/utils.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, - active_reaction, - set_active_reaction -} 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 '../constants.js'; import { proxy } from '../proxy.js'; import { capture_store_binding } from './store.js'; import { legacy_mode_flag } from '../../flags/index.js'; @@ -249,6 +235,14 @@ export function spread_props(...props) { return new Proxy({ props }, spread_props_handler); } +/** + * @param {Derived} current_value + * @returns {boolean} + */ +function has_destroyed_component_ctx(current_value) { + return current_value.ctx?.d ?? false; +} + /** * This function is responsible for synchronizing a possibly bound prop with the inner component state. * It is used whenever the compiler sees that the component writes to the prop, or when it has a default prop_value. @@ -382,6 +376,11 @@ export function prop(props, key, flags, fallback) { 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; return function (/** @type {any} */ value, /** @type {boolean} */ mutation) { @@ -408,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 ee550e4c4d..c2512d0d40 100644 --- a/packages/svelte/src/internal/client/reactivity/sources.js +++ b/packages/svelte/src/internal/client/reactivity/sources.js @@ -14,9 +14,8 @@ import { derived_sources, set_derived_sources, check_dirtiness, - set_is_flushing_effect, - is_flushing_effect, - untracking + untracking, + is_destroying_effect } from '../runtime.js'; import { equals, safe_equals } from './equality.js'; import { @@ -37,6 +36,7 @@ import { proxy } from '../proxy.js'; import { component_context, is_runes } from '../context.js'; export let inspect_effects = new Set(); +export const old_values = new Map(); /** * @param {Set} v @@ -238,6 +238,13 @@ export function simple_set(source, value, should_proxy = false, needs_previous = 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(); @@ -270,22 +277,18 @@ export function internal_set(source, value) { 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(); } diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 75d45d9db9..0a65c6e45a 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -9,7 +9,6 @@ import { } from './reactivity/effects.js'; import { EFFECT, - RENDER_EFFECT, DIRTY, MAYBE_DIRTY, CLEAN, @@ -26,7 +25,7 @@ import { BOUNDARY_EFFECT } from './constants.js'; import { flush_tasks } from './dom/task.js'; -import { internal_set } from './reactivity/sources.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 { FILENAME } from '../../constants.js'; @@ -41,28 +40,19 @@ import { } 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) { @@ -74,7 +64,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 @@ -117,7 +106,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; @@ -370,22 +359,18 @@ export function handle_error(error, effect, previous_effect, component_context) /** * @param {Value} signal * @param {Effect} effect - * @param {number} [depth] + * @param {boolean} [root] */ -function schedule_possible_effect_self_invalidation(signal, effect, depth = 0) { +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.f & DERIVED) !== 0) { - schedule_possible_effect_self_invalidation( - /** @type {Derived} */ (reaction), - effect, - depth + 1 - ); + schedule_possible_effect_self_invalidation(/** @type {Derived} */ (reaction), effect, false); } else if (effect === reaction) { - if (depth === 0) { + if (root) { set_signal_status(reaction, DIRTY); } else if ((reaction.f & CLEAN) !== 0) { set_signal_status(reaction, MAYBE_DIRTY); @@ -414,10 +399,9 @@ export function update_reaction(reaction) { new_deps = /** @type {null | Value[]} */ (null); skipped_deps = 0; untracked_writes = null; - active_reaction = (flags & (BRANCH_EFFECT | ROOT_EFFECT)) === 0 ? reaction : null; skip_reaction = - (flags & UNOWNED) !== 0 && - (!is_flushing_effect || previous_reaction === null || previous_untracking); + (flags & UNOWNED) !== 0 && (untracking || !is_updating_effect || active_reaction === null); + active_reaction = (flags & (BRANCH_EFFECT | ROOT_EFFECT)) === 0 ? reaction : null; derived_sources = null; set_component_context(reaction.ctx); @@ -458,6 +442,8 @@ export function update_reaction(reaction) { 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++) { @@ -474,6 +460,14 @@ export function update_reaction(reaction) { // the same version if (previous_reaction !== null) { 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; @@ -561,8 +555,10 @@ 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; @@ -604,6 +600,7 @@ 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) { @@ -622,69 +619,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(); } - var collected_effects = process_effects(effect); - flush_queued_effects(collected_effects); + var root_effects = queued_root_effects; + var length = root_effects.length; + + queued_root_effects = []; + + for (var i = 0; i < length; i++) { + var collected_effects = process_effects(root_effects[i]); + flush_queued_effects(collected_effects); + } } } 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 = []; + } + old_values.clear(); } } @@ -726,39 +723,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; @@ -780,113 +755,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} root * @returns {Effect[]} */ -function process_effects(effect) { +function process_effects(root) { /** @type {Effect[]} */ var effects = []; - var current_effect = effect.first; + /** @type {Effect | null} */ + var effect = root; - main_loop: while (current_effect !== null) { - var flags = current_effect.f; - var is_branch = (flags & BRANCH_EFFECT) !== 0; + 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 & EFFECT) !== 0) { - effects.push(current_effect); + effects.push(effect); } else if (is_branch) { - current_effect.f ^= CLEAN; + effect.f ^= CLEAN; } else { // Ensure we set the effect to be the active reaction // to ensure that unowned deriveds are correctly tracked // because we're flushing the current effect var previous_active_reaction = active_reaction; try { - active_reaction = current_effect; - if (check_dirtiness(current_effect)) { - update_effect(current_effect); + active_reaction = effect; + if (check_dirtiness(effect)) { + update_effect(effect); } } catch (error) { - handle_error(error, current_effect, null, current_effect.ctx); + 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; + 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; } 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(); +export function flushSync(fn) { + var result; - /** @type {Effect[]} */ - const root_effects = []; - - 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); } /** @@ -895,9 +846,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(); } /** @@ -981,6 +932,10 @@ export function get(signal) { } } + if (is_destroying_effect && old_values.has(signal)) { + return old_values.get(signal); + } + return signal.v; } @@ -999,7 +954,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(); diff --git a/packages/svelte/src/internal/client/types.d.ts b/packages/svelte/src/internal/client/types.d.ts index e7ce4137fb..8c47a17e89 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); 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/index.js b/packages/svelte/src/internal/server/index.js index c4e5d318dc..6098b496c5 100644 --- a/packages/svelte/src/internal/server/index.js +++ b/packages/svelte/src/internal/server/index.js @@ -2,7 +2,7 @@ /** @import { Component, Payload, 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,7 +10,6 @@ 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'; @@ -34,7 +33,9 @@ export function copy_payload({ out, css, head, uid }) { css: new Set(css), head: { title: head.title, - out: head.out + out: head.out, + css: new Set(head.css), + uid: head.uid }, uid }; @@ -85,9 +86,14 @@ export function element(payload, tag, attributes_fn = noop, children_fn = noop) */ export let on_destroy = []; -function props_id_generator() { +/** + * Creates an ID generator + * @param {string} prefix + * @returns {() => string} + */ +function props_id_generator(prefix) { let uid = 1; - return () => 's' + uid++; + return () => `${prefix}s${uid++}`; } /** @@ -95,16 +101,17 @@ function props_id_generator() { * 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, uid?: () => string }} [options] + * @param {{ props?: Omit; context?: Map; idPrefix?: string }} [options] * @returns {RenderOutput} */ export function render(component, options = {}) { + const uid = props_id_generator(options.idPrefix ? options.idPrefix + '-' : ''); /** @type {Payload} */ const payload = { out: '', css: new Set(), - head: { title: '', out: '' }, - uid: options.uid ?? props_id_generator() + head: { title: '', out: '', css: new Set(), uid }, + uid }; const prev_on_destroy = on_destroy; @@ -195,32 +202,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 = ''; @@ -286,35 +284,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)}"` : ''; } /** diff --git a/packages/svelte/src/internal/server/types.d.ts b/packages/svelte/src/internal/server/types.d.ts index 8a241deecd..2fffdbbdf0 100644 --- a/packages/svelte/src/internal/server/types.d.ts +++ b/packages/svelte/src/internal/server/types.d.ts @@ -17,6 +17,8 @@ export interface Payload { head: { title: string; out: string; + uid: () => string; + css: Set<{ hash: string; code: string }>; }; /** Function that generates a unique ID */ uid: () => 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/legacy/legacy-client.js b/packages/svelte/src/legacy/legacy-client.js index 3a05bc0496..bb9a5a9c03 100644 --- a/packages/svelte/src/legacy/legacy-client.js +++ b/packages/svelte/src/legacy/legacy-client.js @@ -3,7 +3,7 @@ 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, 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'; @@ -119,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/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/version.js b/packages/svelte/src/version.js index ada6f9019d..32a50f3bce 100644 --- a/packages/svelte/src/version.js +++ b/packages/svelte/src/version.js @@ -4,5 +4,5 @@ * The current version, as set in package.json. * @type {string} */ -export const VERSION = '5.19.10'; +export const VERSION = '5.23.1'; export const PUBLIC_VERSION = '5'; 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/has/_config.js b/packages/svelte/tests/css/samples/has/_config.js index 8d89d98cbd..5700a09b96 100644 --- a/packages/svelte/tests/css/samples/has/_config.js +++ b/packages/svelte/tests/css/samples/has/_config.js @@ -6,210 +6,238 @@ export default test({ code: 'css_unused_selector', message: 'Unused CSS selector ".unused:has(y)"', start: { - line: 33, + line: 41, column: 1, - character: 330 + character: 378 }, end: { - line: 33, + line: 41, column: 15, - character: 344 + character: 392 } }, { code: 'css_unused_selector', message: 'Unused CSS selector ".unused:has(:global(y))"', start: { - line: 36, + line: 44, column: 1, - character: 365 + character: 413 }, end: { - line: 36, + line: 44, column: 24, - character: 388 + character: 436 } }, { code: 'css_unused_selector', message: 'Unused CSS selector "x:has(.unused)"', start: { - line: 39, + line: 47, column: 1, - character: 409 + character: 457 }, end: { - line: 39, + line: 47, column: 15, - character: 423 + character: 471 } }, { code: 'css_unused_selector', message: 'Unused CSS selector ":global(.foo):has(.unused)"', start: { - line: 42, + line: 50, column: 1, - character: 444 + character: 492 }, end: { - line: 42, + line: 50, column: 27, - character: 470 + character: 518 } }, { code: 'css_unused_selector', message: 'Unused CSS selector "x:has(y):has(.unused)"', start: { - line: 52, + line: 60, column: 1, - character: 578 + character: 626 }, end: { - line: 52, + line: 60, column: 22, - character: 599 + character: 647 } }, { code: 'css_unused_selector', message: 'Unused CSS selector ".unused"', start: { - line: 71, + line: 79, column: 2, - character: 804 + character: 852 }, end: { - line: 71, + line: 79, column: 9, - character: 811 + character: 859 } }, { code: 'css_unused_selector', message: 'Unused CSS selector ".unused x:has(y)"', start: { - line: 87, + line: 95, column: 1, - character: 958 + character: 1006 }, end: { - line: 87, + line: 95, column: 17, - character: 974 + character: 1022 } }, { code: 'css_unused_selector', message: 'Unused CSS selector ".unused:has(.unused)"', start: { - line: 90, + line: 98, column: 1, - character: 995 + character: 1043 }, end: { - line: 90, + line: 98, column: 21, - character: 1015 + character: 1063 } }, { code: 'css_unused_selector', message: 'Unused CSS selector "x:has(> z)"', start: { - line: 100, + line: 108, column: 1, - character: 1115 + character: 1163 }, end: { - line: 100, + line: 108, column: 11, - character: 1125 + character: 1173 } }, { code: 'css_unused_selector', message: 'Unused CSS selector "x:has(> d)"', start: { - line: 103, + line: 111, column: 1, - character: 1146 + character: 1194 }, end: { - line: 103, + line: 111, column: 11, - character: 1156 + character: 1204 } }, { code: 'css_unused_selector', message: 'Unused CSS selector "x:has(~ y)"', start: { - line: 123, + line: 131, column: 1, - character: 1348 + character: 1396 }, end: { - line: 123, + line: 131, column: 11, - character: 1358 + character: 1406 + } + }, + { + code: 'css_unused_selector', + message: 'Unused CSS selector "d:has(+ f)"', + start: { + line: 141, + column: 1, + character: 1494 + }, + end: { + line: 141, + column: 11, + character: 1504 } }, { code: 'css_unused_selector', message: 'Unused CSS selector "f:has(~ d)"', start: { - line: 133, + line: 144, column: 1, - character: 1446 + character: 1525 }, end: { - line: 133, + line: 144, column: 11, - character: 1456 + character: 1535 } }, { code: 'css_unused_selector', message: 'Unused CSS selector ":has(.unused)"', start: { - line: 141, + line: 152, column: 2, - character: 1529 + character: 1608 }, end: { - line: 141, + line: 152, column: 15, - character: 1542 + character: 1621 } }, { code: 'css_unused_selector', message: 'Unused CSS selector "&:has(.unused)"', start: { - line: 147, + line: 158, column: 2, - character: 1600 + character: 1679 }, end: { - line: 147, + line: 158, column: 16, - character: 1614 + character: 1693 } }, { code: 'css_unused_selector', message: 'Unused CSS selector ":global(.foo):has(.unused)"', start: { - line: 155, + line: 166, column: 1, - character: 1684 + character: 1763 }, end: { - line: 155, + line: 166, column: 27, - character: 1710 + 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 b257370d61..2ce4d2bec5 100644 --- a/packages/svelte/tests/css/samples/has/expected.css +++ b/packages/svelte/tests/css/samples/has/expected.css @@ -118,6 +118,9 @@ d.svelte-xyz:has(~ f:where(.svelte-xyz)) { color: green; } + /* (unused) d:has(+ f) { + color: red; + }*/ /* (unused) f:has(~ d) { color: red; }*/ @@ -143,3 +146,13 @@ /* (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 9b254996bf..033471bc16 100644 --- a/packages/svelte/tests/css/samples/has/input.svelte +++ b/packages/svelte/tests/css/samples/has/input.svelte @@ -9,6 +9,14 @@ + + + + + + + + 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/undefined-with-scope/expected.html b/packages/svelte/tests/css/samples/undefined-with-scope/expected.html index ddb9429bc8..5eecaa9bb2 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 @@ -

Foo

\ No newline at end of file +

Foo

\ No newline at end of file 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/helpers.js b/packages/svelte/tests/helpers.js index 9d7f71c9bd..87bcb473e7 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'); 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/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/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/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/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/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/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 @@ + + + + +{#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} + 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}

+ + 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 @@ + + + + + + +{#if count < 2} + +{/if} + + +{#if count < 2} + +{/if} + + +{#if count < 2} + +{/if} + + +{#if show} + +{/if} + + + + + + + + + + + + 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/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/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/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
diff --git a/packages/svelte/tests/runtime-runes/samples/each-bind-store-no-warning/_config.js b/packages/svelte/tests/runtime-runes/samples/each-bind-store-no-warning/_config.js new file mode 100644 index 0000000000..dba2e85650 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/each-bind-store-no-warning/_config.js @@ -0,0 +1,10 @@ +import { test } from '../../test'; + +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-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 @@ + + + + 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 @@ + + + + + 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 @@ + + + + + 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/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, + ` + +

c1

+

c2

+

c3

+

c4

+ ` + ); + } else { + assert.htmlEqual( + target.innerHTML, + ` + +

myPrefix-s1

+

myPrefix-s2

+

myPrefix-s3

+

myPrefix-s4

+ ` + ); + } + + let button = target.querySelector('button'); + flushSync(() => button?.click()); + + if (variant === 'dom') { + assert.htmlEqual( + target.innerHTML, + ` + +

c1

+

c2

+

c3

+

c4

+

c5

+ ` + ); + } else { + assert.htmlEqual( + target.innerHTML, + ` + +

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 @@ + + + + +

{id}

+ + + + + +{#if show} + +{/if} diff --git a/packages/svelte/tests/runtime-runes/samples/props-id/_config.js b/packages/svelte/tests/runtime-runes/samples/props-id/_config.js index 9d91b98e0f..416ef6cfbe 100644 --- a/packages/svelte/tests/runtime-runes/samples/props-id/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/props-id/_config.js @@ -43,8 +43,6 @@ export default test({ ` ); } else { - // `c6` because this runs after the `dom` tests - // (slightly brittle but good enough for now) assert.htmlEqual( target.innerHTML, ` @@ -53,7 +51,7 @@ export default test({

s2

s3

s4

-

c6

+

c1

` ); } 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/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/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-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, ``); + } +}); 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 @@ + + + + + +{#if data} + +{/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..e2942b21f3 100644 --- a/packages/svelte/tests/runtime-runes/samples/typescript/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/typescript/main.svelte @@ -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 { 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/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/snapshot/_config.js b/packages/svelte/tests/server-side-rendering/samples/head-component-props-id/_config.js similarity index 100% rename from packages/svelte/tests/snapshot/_config.js rename to packages/svelte/tests/server-side-rendering/samples/head-component-props-id/_config.js 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 046f833e0e..ef4cf16d3b 100644 --- a/packages/svelte/tests/signals/test.ts +++ b/packages/svelte/tests/signals/test.ts @@ -1135,4 +1135,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/dynamic-attributes-casing/_expected/client/main.svelte.js b/packages/svelte/tests/snapshot/samples/dynamic-attributes-casing/_expected/client/main.svelte.js index d97a58bf40..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 @@ -21,13 +21,13 @@ export default function Main($$anchor) { $.template_effect(() => $.set_custom_element_data(custom_element_1, 'fooBar', y())); $.template_effect( - ($0) => { + ($0, $1) => { $.set_attribute(div, 'foobar', x); $.set_attribute(svg, 'viewBox', x); $.set_attribute(div_1, 'foobar', $0); - $.set_attribute(svg_1, 'viewBox', $0); + $.set_attribute(svg_1, 'viewBox', $1); }, - [y] + [y, y] ); $.append($$anchor, fragment); diff --git a/packages/svelte/tests/snapshot/test.ts b/packages/svelte/tests/snapshot/test.ts index 88cf9193c3..0a591c6e2a 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'; @@ -18,8 +18,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/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/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={() => {}}>
+ {}}>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/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/multiple-var-same-name/errors.json b/packages/svelte/tests/validator/samples/multiple-var-same-name/errors.json new file mode 100644 index 0000000000..fe51488c70 --- /dev/null +++ b/packages/svelte/tests/validator/samples/multiple-var-same-name/errors.json @@ -0,0 +1 @@ +[] diff --git a/packages/svelte/tests/validator/samples/multiple-var-same-name/input.svelte b/packages/svelte/tests/validator/samples/multiple-var-same-name/input.svelte new file mode 100644 index 0000000000..19a8ef7722 --- /dev/null +++ b/packages/svelte/tests/validator/samples/multiple-var-same-name/input.svelte @@ -0,0 +1,6 @@ + + +{test} \ No newline at end of file diff --git a/packages/svelte/tests/validator/samples/reassign-derived-private-public-field/errors.json b/packages/svelte/tests/validator/samples/reassign-derived-private-public-field/errors.json new file mode 100644 index 0000000000..fe51488c70 --- /dev/null +++ b/packages/svelte/tests/validator/samples/reassign-derived-private-public-field/errors.json @@ -0,0 +1 @@ +[] diff --git a/packages/svelte/tests/validator/samples/reassign-derived-private-public-field/input.svelte b/packages/svelte/tests/validator/samples/reassign-derived-private-public-field/input.svelte new file mode 100644 index 0000000000..92f3564920 --- /dev/null +++ b/packages/svelte/tests/validator/samples/reassign-derived-private-public-field/input.svelte @@ -0,0 +1,13 @@ + \ No newline at end of file diff --git a/packages/svelte/tsconfig.json b/packages/svelte/tsconfig.json index c9f0fb3b2b..76005add13 100644 --- a/packages/svelte/tsconfig.json +++ b/packages/svelte/tsconfig.json @@ -15,7 +15,6 @@ "allowJs": true, "checkJs": true, "paths": { - "acorn-typescript": ["./src/compiler/phases/1-parse/ambient.d.ts"], "svelte": ["./src/index.d.ts"], "svelte/action": ["./src/action/public.d.ts"], "svelte/compiler": ["./src/compiler/public.d.ts"], diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index b68e166de5..0de66ff136 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -16,6 +16,7 @@ declare module 'svelte' { intro?: boolean; recover?: boolean; sync?: boolean; + idPrefix?: string; $$inline?: boolean; } @@ -348,13 +349,14 @@ declare module 'svelte' { props: Props; }); /** - * 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). * * */ export function onMount(fn: () => NotFunction | Promise> | (() => any)): void; @@ -408,13 +410,7 @@ declare module 'svelte' { * @deprecated Use [`$effect`](https://svelte.dev/docs/svelte/$effect) instead * */ export function afterUpdate(fn: () => void): void; - /** - * Synchronously flushes any pending state changes and those that result from it. - * */ - export function flushSync(fn?: (() => void) | undefined): void; - type Getters = { - [K in keyof T]: () => T[K]; - }; + export interface StateOptions { onchange?: () => unknown; } @@ -427,6 +423,29 @@ declare module 'svelte' { }): Snippet; /** Anything except a function */ type NotFunction = T extends Function ? never : T; + /** + * Synchronously flush any pending updates. + * Returns void if no callback is provided, otherwise returns the result of calling the callback. + * */ + export function flushSync(fn?: (() => T) | undefined): T; + /** + * Returns a promise that resolves once any pending state changes have been applied. + * */ + export function tick(): Promise; + /** + * When used inside a [`$derived`](https://svelte.dev/docs/svelte/$derived) or [`$effect`](https://svelte.dev/docs/svelte/$effect), + * any state read inside `fn` will not be treated as a dependency. + * + * ```ts + * $effect(() => { + * // this will run when `data` changes, but not when `time` changes + * save(data, { + * timestamp: untrack(() => time) + * }); + * }); + * ``` + * */ + export function untrack(fn: () => T): T; /** * Retrieves the context that belongs to the closest parent component with the specified `key`. * Must be called during component initialisation. @@ -500,24 +519,6 @@ declare module 'svelte' { export function unmount(component: Record, options?: { outro?: boolean; } | undefined): Promise; - /** - * Returns a promise that resolves once any pending state changes have been applied. - * */ - export function tick(): Promise; - /** - * When used inside a [`$derived`](https://svelte.dev/docs/svelte/$derived) or [`$effect`](https://svelte.dev/docs/svelte/$effect), - * any state read inside `fn` will not be treated as a dependency. - * - * ```ts - * $effect(() => { - * // this will run when `data` changes, but not when `time` changes - * save(data, { - * timestamp: untrack(() => time) - * }); - * }); - * ``` - * */ - export function untrack(fn: () => T): T; export {}; } @@ -625,8 +626,8 @@ declare module 'svelte/animate' { } declare module 'svelte/compiler' { - import type { Expression, Identifier, ArrayExpression, ArrowFunctionExpression, VariableDeclaration, VariableDeclarator, MemberExpression, Node, ObjectExpression, Pattern, Program, ChainExpression, SimpleCallExpression, SequenceExpression } from 'estree'; import type { SourceMap } from 'magic-string'; + import type { ArrayExpression, ArrowFunctionExpression, VariableDeclaration, VariableDeclarator, Expression, Identifier, MemberExpression, Node, ObjectExpression, Pattern, Program, ChainExpression, SimpleCallExpression, SequenceExpression } from 'estree'; import type { Location } from 'locate-character'; /** * `compile` converts your `.svelte` source code into a JavaScript module that exports a component @@ -2082,11 +2083,19 @@ declare module 'svelte/server' { ...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; interface RenderOutput { diff --git a/playgrounds/sandbox/package.json b/playgrounds/sandbox/package.json index b5bd8ab2f9..9e2e5f6738 100644 --- a/playgrounds/sandbox/package.json +++ b/playgrounds/sandbox/package.json @@ -17,7 +17,7 @@ "@sveltejs/vite-plugin-svelte": "^4.0.0-next.6", "polka": "^1.0.0-next.25", "svelte": "workspace:*", - "tiny-glob": "^0.2.9", + "tinyglobby": "^0.2.12", "vite": "^5.4.14", "vite-plugin-inspect": "^0.8.4" } diff --git a/playgrounds/sandbox/run.js b/playgrounds/sandbox/run.js index 4cb8314532..8fa2c2a2da 100644 --- a/playgrounds/sandbox/run.js +++ b/playgrounds/sandbox/run.js @@ -2,7 +2,7 @@ import * as fs from 'node:fs'; import * as path from 'node:path'; import { fileURLToPath } from 'node:url'; import { parseArgs } from 'node:util'; -import glob from 'tiny-glob/sync.js'; +import { globSync } from 'tinyglobby'; import { compile, compileModule, parse, migrate } from 'svelte/compiler'; const argv = parseArgs({ options: { runes: { type: 'boolean' } }, args: process.argv.slice(2) }); @@ -35,8 +35,8 @@ function write(file, contents) { fs.writeFileSync(file, contents); } -const svelte_modules = glob('**/*.svelte', { cwd: `${cwd}/src` }); -const js_modules = glob('**/*.js', { cwd: `${cwd}/src` }); +const svelte_modules = globSync('**/*.svelte', { cwd: `${cwd}/src` }); +const js_modules = globSync('**/*.js', { cwd: `${cwd}/src` }); for (const generate of /** @type {const} */ (['client', 'server'])) { console.error(`\n--- generating ${generate} ---\n`); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f7700cd65f..c687db12d4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -13,7 +13,7 @@ importers: version: 2.27.8 '@sveltejs/eslint-config': specifier: ^8.1.0 - version: 8.1.0(@stylistic/eslint-plugin-js@1.8.0(eslint@9.9.1))(eslint-config-prettier@9.1.0(eslint@9.9.1))(eslint-plugin-n@17.9.0(eslint@9.9.1))(eslint-plugin-svelte@2.38.0(eslint@9.9.1)(svelte@packages+svelte))(eslint@9.9.1)(typescript-eslint@8.2.0(eslint@9.9.1)(typescript@5.5.4))(typescript@5.5.4) + version: 8.1.0(@stylistic/eslint-plugin-js@1.8.0(eslint@9.9.1))(eslint-config-prettier@9.1.0(eslint@9.9.1))(eslint-plugin-n@17.16.1(eslint@9.9.1)(typescript@5.5.4))(eslint-plugin-svelte@2.38.0(eslint@9.9.1)(svelte@packages+svelte))(eslint@9.9.1)(typescript-eslint@8.26.0(eslint@9.9.1)(typescript@5.5.4))(typescript@5.5.4) '@svitejs/changesets-changelog-github-compact': specifier: ^1.1.0 version: 1.1.0 @@ -48,8 +48,8 @@ importers: specifier: ^5.5.4 version: 5.5.4 typescript-eslint: - specifier: ^8.2.0 - version: 8.2.0(eslint@9.9.1)(typescript@5.5.4) + specifier: ^8.24.0 + version: 8.26.0(eslint@9.9.1)(typescript@5.5.4) v8-natives: specifier: ^1.2.5 version: 1.2.5 @@ -65,15 +65,15 @@ importers: '@jridgewell/sourcemap-codec': specifier: ^1.5.0 version: 1.5.0 + '@sveltejs/acorn-typescript': + specifier: ^1.0.5 + version: 1.0.5(acorn@8.14.0) '@types/estree': specifier: ^1.0.5 version: 1.0.6 acorn: specifier: ^8.12.1 - version: 8.12.1 - acorn-typescript: - specifier: ^1.4.13 - version: 1.4.13(acorn@8.12.1) + version: 8.14.0 aria-query: specifier: ^5.3.1 version: 5.3.1 @@ -97,7 +97,7 @@ importers: version: 3.0.0 magic-string: specifier: ^0.30.11 - version: 0.30.11 + version: 0.30.17 zimmerframe: specifier: ^1.1.2 version: 1.1.2 @@ -127,8 +127,8 @@ importers: specifier: ^20.11.5 version: 20.12.7 dts-buddy: - specifier: ^0.5.3 - version: 0.5.3(typescript@5.5.4) + specifier: ^0.5.5 + version: 0.5.5(typescript@5.5.4) esbuild: specifier: ^0.21.5 version: 0.21.5 @@ -138,9 +138,9 @@ importers: source-map: specifier: ^0.7.4 version: 0.7.4 - tiny-glob: - specifier: ^0.2.9 - version: 0.2.9 + tinyglobby: + specifier: ^0.2.12 + version: 0.2.12 typescript: specifier: ^5.5.4 version: 5.5.4 @@ -159,9 +159,9 @@ importers: svelte: specifier: workspace:* version: link:../../packages/svelte - tiny-glob: - specifier: ^0.2.9 - version: 0.2.9 + tinyglobby: + specifier: ^0.2.12 + version: 0.2.12 vite: specifier: ^5.4.14 version: 5.4.14(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0) @@ -402,22 +402,12 @@ packages: cpu: [x64] os: [win32] - '@eslint-community/eslint-utils@4.4.0': - resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 - '@eslint-community/eslint-utils@4.4.1': resolution: {integrity: sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 - '@eslint-community/regexpp@4.11.0': - resolution: {integrity: sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A==} - engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} - '@eslint-community/regexpp@4.12.1': resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} @@ -636,6 +626,11 @@ packages: peerDependencies: eslint: '>=8.40.0' + '@sveltejs/acorn-typescript@1.0.5': + resolution: {integrity: sha512-IwQk4yfwLdibDlrXVE04jTZYlLnwsTT2PIOQQGNLWfjavGifnk1JD1LcZjZaBTRcxZu2FfPfNLOE04DSu9lqtQ==} + peerDependencies: + acorn: ^8.9.0 + '@sveltejs/eslint-config@8.1.0': resolution: {integrity: sha512-cfgp4lPREYBjNd4ZzaP/jA85ufm7vfXiaV7h9vILXNogne80IbZRNhRCQ8XoOqTAOY/pChIzWTBuR8aDNMbAEA==} peerDependencies: @@ -693,61 +688,51 @@ packages: '@types/semver@7.5.6': resolution: {integrity: sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A==} - '@typescript-eslint/eslint-plugin@8.2.0': - resolution: {integrity: sha512-02tJIs655em7fvt9gps/+4k4OsKULYGtLBPJfOsmOq1+3cdClYiF0+d6mHu6qDnTcg88wJBkcPLpQhq7FyDz0A==} + '@typescript-eslint/eslint-plugin@8.26.0': + resolution: {integrity: sha512-cLr1J6pe56zjKYajK6SSSre6nl1Gj6xDp1TY0trpgPzjVbgDwd09v2Ws37LABxzkicmUjhEeg/fAUjPJJB1v5Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: '@typescript-eslint/parser': ^8.0.0 || ^8.0.0-alpha.0 eslint: ^8.57.0 || ^9.0.0 - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true + typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/parser@8.2.0': - resolution: {integrity: sha512-j3Di+o0lHgPrb7FxL3fdEy6LJ/j2NE8u+AP/5cQ9SKb+JLH6V6UHDqJ+e0hXBkHP1wn1YDFjYCS9LBQsZDlDEg==} + '@typescript-eslint/parser@8.26.0': + resolution: {integrity: sha512-mNtXP9LTVBy14ZF3o7JG69gRPBK/2QWtQd0j0oH26HcY/foyJJau6pNUez7QrM5UHnSvwlQcJXKsk0I99B9pOA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true + typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/scope-manager@8.2.0': - resolution: {integrity: sha512-OFn80B38yD6WwpoHU2Tz/fTz7CgFqInllBoC3WP+/jLbTb4gGPTy9HBSTsbDWkMdN55XlVU0mMDYAtgvlUspGw==} + '@typescript-eslint/scope-manager@8.26.0': + resolution: {integrity: sha512-E0ntLvsfPqnPwng8b8y4OGuzh/iIOm2z8U3S9zic2TeMLW61u5IH2Q1wu0oSTkfrSzwbDJIB/Lm8O3//8BWMPA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/type-utils@8.2.0': - resolution: {integrity: sha512-g1CfXGFMQdT5S+0PSO0fvGXUaiSkl73U1n9LTK5aRAFnPlJ8dLKkXr4AaLFvPedW8lVDoMgLLE3JN98ZZfsj0w==} + '@typescript-eslint/type-utils@8.26.0': + resolution: {integrity: sha512-ruk0RNChLKz3zKGn2LwXuVoeBcUMh+jaqzN461uMMdxy5H9epZqIBtYj7UiPXRuOpaALXGbmRuZQhmwHhaS04Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/types@8.2.0': - resolution: {integrity: sha512-6a9QSK396YqmiBKPkJtxsgZZZVjYQ6wQ/TlI0C65z7vInaETuC6HAHD98AGLC8DyIPqHytvNuS8bBVvNLKyqvQ==} + '@typescript-eslint/types@8.26.0': + resolution: {integrity: sha512-89B1eP3tnpr9A8L6PZlSjBvnJhWXtYfZhECqlBl1D9Lme9mHO6iWlsprBtVenQvY1HMhax1mWOjhtL3fh/u+pA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/typescript-estree@8.2.0': - resolution: {integrity: sha512-kiG4EDUT4dImplOsbh47B1QnNmXSoUqOjWDvCJw/o8LgfD0yr7k2uy54D5Wm0j4t71Ge1NkynGhpWdS0dEIAUA==} + '@typescript-eslint/typescript-estree@8.26.0': + resolution: {integrity: sha512-tiJ1Hvy/V/oMVRTbEOIeemA2XoylimlDQ03CgPPNaHYZbpsc78Hmngnt+WXZfJX1pjQ711V7g0H7cSJThGYfPQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true + typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/utils@8.2.0': - resolution: {integrity: sha512-O46eaYKDlV3TvAVDNcoDzd5N550ckSe8G4phko++OCSC1dYIb9LTc3HDGYdWqWIAT5qDUKphO6sd9RrpIJJPfg==} + '@typescript-eslint/utils@8.26.0': + resolution: {integrity: sha512-2L2tU3FVwhvU14LndnQCA2frYC8JnPDVKyQtWFPf8IYFMt/ykEN1bPolNhNbCVgOmdzTlWdusCTKA/9nKrf8Ig==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/visitor-keys@8.2.0': - resolution: {integrity: sha512-sbgsPMW9yLvS7IhCi8IpuK1oBmtbWUNP+hBdwl/I9nzqVsszGnNGti5r9dUtF5RLivHUFFIdRvLiTsPhzSyJ3Q==} + '@typescript-eslint/visitor-keys@8.26.0': + resolution: {integrity: sha512-2z8JQJWAzPdDd51dRQ/oqIJxe99/hoLIqmf8RMCAJQtYDc535W/Jt2+RTP4bP0aKeBG1F65yjIZuczOXCmbWwg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@vitest/coverage-v8@2.0.5': @@ -789,16 +774,6 @@ packages: peerDependencies: acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 - acorn-typescript@1.4.13: - resolution: {integrity: sha512-xsc9Xv0xlVfwp2o7sQ+GCQ1PgbkdcpWdTzrwXxO3xDMTAywVS3oXVOcOHuRjAPkS4P9b+yc/qNF15460v+jp4Q==} - peerDependencies: - acorn: '>=8.9.0' - - acorn@8.12.1: - resolution: {integrity: sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==} - engines: {node: '>=0.4.0'} - hasBin: true - acorn@8.14.0: resolution: {integrity: sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==} engines: {node: '>=0.4.0'} @@ -877,10 +852,6 @@ packages: brace-expansion@2.0.1: resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} - braces@3.0.2: - resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==} - engines: {node: '>=8'} - braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} @@ -945,7 +916,7 @@ packages: resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==} concat-map@0.0.1: - resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + resolution: {integrity: sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=} cross-spawn@5.1.0: resolution: {integrity: sha512-pTgQJ5KC0d2hcY8eyL1IzlBPYjTkyH72XRZPnLyKus2mBfNjQs3klqbJU2VILqZryAZUt9JOb3h/mWMy23/f5A==} @@ -970,15 +941,6 @@ packages: dataloader@1.4.0: resolution: {integrity: sha512-68s5jYdlvasItOJnCuI2Q9s4q98g0pCyL3HrcKJu8KNugUl8ahgmZYg38ysLTgQjjXX3H8CJLkAvWrclWfcalw==} - debug@4.3.6: - resolution: {integrity: sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==} - engines: {node: '>=6.0'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - debug@4.4.0: resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==} engines: {node: '>=6.0'} @@ -1035,11 +997,11 @@ packages: resolution: {integrity: sha512-HTlk5nmhkm8F6JcdXvHIzaorzCoziNQT9mGxLPVXW8wJF1TiGSL60ZGB4gHWabHOaMmWmhvk2/lPHfnBiT78AQ==} engines: {node: '>=12'} - dts-buddy@0.5.3: - resolution: {integrity: sha512-wS2DC5T+F6R+sG/YNlJ21yn8CKVhy1QQlpKA34G+uO4PUXkwz+JQWbGcIryUByxoJgbH98O0dTGzE2RqsRR3KA==} + dts-buddy@0.5.5: + resolution: {integrity: sha512-Mu5PJuP7C+EqZIwDtW/bG1tVli1UFhRIyW/dERBVBYk28OviTkribu9S2LpDQ0HF2MbkqnjQIkbbE6HnepdNTQ==} hasBin: true peerDependencies: - typescript: '>=5.0.4 <5.6' + typescript: '>=5.0.4 <5.8' eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} @@ -1098,8 +1060,8 @@ packages: eslint-plugin-lube@0.4.3: resolution: {integrity: sha512-BVO83tRo090d6a04cl45Gb761SD79cOT6wKxxWrpsH7Rv8I0SJvc79ijE11vvyxxCMiGUVq/w4NqqPJAHyYfSQ==} - eslint-plugin-n@17.9.0: - resolution: {integrity: sha512-CPSaXDXdrT4nsrOrO4mT4VB6FMUkoySRkHWuuJJHVqsIEjIeZgMY1H7AzSwPbDScikBmLN82KeM1u7ixV7PzGg==} + eslint-plugin-n@17.16.1: + resolution: {integrity: sha512-/7FVAwjUrix9P5lycnsYRIQRwFo/DZROD+ZXWLpE+/EZWLyuLvyFaRdAPYJSz+nlAdZIZp+LAzlBerQSVYUNFg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: '>=8.23.0' @@ -1126,8 +1088,8 @@ packages: resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - eslint-visitor-keys@4.0.0: - resolution: {integrity: sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==} + eslint-visitor-keys@4.2.0: + resolution: {integrity: sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} eslint@9.9.1: @@ -1208,8 +1170,8 @@ packages: fastq@1.16.0: resolution: {integrity: sha512-ifCoaXsDrsdkWTtiNJX5uzHDsrck5TzfKKDcuFFTIrrc/BS076qgEIfoIy1VeZqViznfKiysPYTh/QeHtnIsYA==} - fdir@6.3.0: - resolution: {integrity: sha512-QOnuT+BOtivR77wYvCWHfGt9s4Pz1VIMbD463vegT5MLqNXy8rYFT/lPVEqf/bhYeT6qmqrNHhsX+rWwe3rOCQ==} + fdir@6.4.3: + resolution: {integrity: sha512-PMXmW2y1hDDfTSRc9gaXIuCCRpuoz3Kaz8cUelp3smouvfT632ozg2vrT6lJsHKKOF59YLbOGfAWGUcKEfRMQw==} peerDependencies: picomatch: ^3 || ^4 peerDependenciesMeta: @@ -1220,10 +1182,6 @@ packages: resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} engines: {node: '>=16.0.0'} - fill-range@7.0.1: - resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} - engines: {node: '>=8'} - fill-range@7.1.1: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} @@ -1299,20 +1257,10 @@ packages: resolution: {integrity: sha512-OkToC372DtlQeje9/zHIo5CT8lRP/FUgEOKBEhU4e0abL7J7CD24fD9ohiLN5hagG/kWCYj4K5oaxxtj2Z0Dig==} engines: {node: '>=18'} - globals@15.9.0: - resolution: {integrity: sha512-SmSKyLLKFbSr6rptvP8izbyxJL4ILwqO9Jg23UA0sDlGlu58V59D1//I3vlc0KJphVdUR7vMjHIplYnzBxorQA==} - engines: {node: '>=18'} - - globalyzer@0.1.0: - resolution: {integrity: sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q==} - globby@11.1.0: resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} engines: {node: '>=10'} - globrex@0.1.2: - resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==} - graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} @@ -1585,9 +1533,6 @@ packages: lru-cache@4.1.5: resolution: {integrity: sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==} - magic-string@0.30.11: - resolution: {integrity: sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==} - magic-string@0.30.17: resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} @@ -1614,6 +1559,10 @@ packages: resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} engines: {node: '>= 0.6'} + minimatch@10.0.1: + resolution: {integrity: sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==} + engines: {node: 20 || >=22} + minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} @@ -1633,17 +1582,9 @@ packages: resolution: {integrity: sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==} engines: {node: '>=10'} - ms@2.1.2: - resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} - ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - nanoid@3.3.7: - resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} - engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} - hasBin: true - nanoid@3.3.8: resolution: {integrity: sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -1753,9 +1694,6 @@ packages: perfect-debounce@1.0.0: resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==} - picocolors@1.1.0: - resolution: {integrity: sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==} - picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -1813,10 +1751,6 @@ packages: resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==} engines: {node: '>=4'} - postcss@8.4.47: - resolution: {integrity: sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==} - engines: {node: ^10 || ^12 || >=14} - postcss@8.5.1: resolution: {integrity: sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ==} engines: {node: ^10 || ^12 || >=14} @@ -1922,11 +1856,6 @@ packages: resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} engines: {node: '>=v12.22.7'} - semver@7.6.3: - resolution: {integrity: sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==} - engines: {node: '>=10'} - hasBin: true - semver@7.7.1: resolution: {integrity: sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==} engines: {node: '>=10'} @@ -1996,9 +1925,6 @@ packages: stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} - std-env@3.7.0: - resolution: {integrity: sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==} - std-env@3.8.0: resolution: {integrity: sha512-Bc3YwwCB+OzldMxOXJIIvC6cPRWr/LxOp48CdQTOkPyk/t4JWWJbrilwBd7RJzKV8QW7tJkcgAmeuLLJugl5/w==} @@ -2066,15 +1992,16 @@ packages: text-table@0.2.0: resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} - tiny-glob@0.2.9: - resolution: {integrity: sha512-g/55ssRPUjShh+xkfx9UPDXqhckHEsHr4Vd9zX55oSdGZc/MD0m3sferOkwWtp98bv+kcVfEHtRJgBVJzelrzg==} - tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} tinyexec@0.3.2: resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + tinyglobby@0.2.12: + resolution: {integrity: sha512-qkf4trmKSIiMTs/E63cxH+ojC2unam7rJ0WrauAzpT3ECNTxGRMlaXxVbfxMUC/w0LaYk6jQ4y/nGR9uBO3tww==} + engines: {node: '>=12.0.0'} + tinypool@1.0.2: resolution: {integrity: sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA==} engines: {node: ^18.0.0 || >=20.0.0} @@ -2131,18 +2058,27 @@ packages: peerDependencies: typescript: '>=4.2.0' + ts-api-utils@2.0.1: + resolution: {integrity: sha512-dnlgjFSVetynI8nzgJ+qF62efpglpWRk8isUEWZGWlJYySCTD6aKvbUDu+zbPeDakk3bg5H4XpitHukgfL1m9w==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + + ts-declaration-location@1.0.5: + resolution: {integrity: sha512-WqmlO9IoeYwCqJ2E9kHMcY9GZhhfLYItC3VnHDlPOrg6nNdUWS4wn4hhDZUPt60m1EvtjPIZyprTjpI992Bgzw==} + peerDependencies: + typescript: '>=4.0.0' + type-check@0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} - typescript-eslint@8.2.0: - resolution: {integrity: sha512-DmnqaPcML0xYwUzgNbM1XaKXpEb7BShYf2P1tkUmmcl8hyeG7Pj08Er7R9bNy6AufabywzJcOybQAtnD/c9DGw==} + typescript-eslint@8.26.0: + resolution: {integrity: sha512-PtVz9nAnuNJuAVeUFvwztjuUgSnJInODAUx47VDwWPXzd5vismPOtPtt83tzNXyOjVQbPRp786D6WFW/M2koIA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <5.9.0' typescript@5.5.4: resolution: {integrity: sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==} @@ -2375,7 +2311,7 @@ snapshots: outdent: 0.5.0 prettier: 2.8.8 resolve-from: 5.0.0 - semver: 7.6.3 + semver: 7.7.1 '@changesets/assemble-release-plan@6.0.4': dependencies: @@ -2384,7 +2320,7 @@ snapshots: '@changesets/should-skip-package': 0.1.1 '@changesets/types': 6.0.0 '@manypkg/get-packages': 1.1.3 - semver: 7.6.3 + semver: 7.7.1 '@changesets/changelog-git@0.2.0': dependencies: @@ -2417,9 +2353,9 @@ snapshots: outdent: 0.5.0 p-limit: 2.3.0 package-manager-detector: 0.2.0 - picocolors: 1.1.0 + picocolors: 1.1.1 resolve-from: 5.0.0 - semver: 7.6.3 + semver: 7.7.1 spawndamnit: 2.0.0 term-size: 2.2.1 @@ -2441,8 +2377,8 @@ snapshots: dependencies: '@changesets/types': 6.0.0 '@manypkg/get-packages': 1.1.3 - picocolors: 1.1.0 - semver: 7.6.3 + picocolors: 1.1.1 + semver: 7.7.1 '@changesets/get-github-info@0.5.2': dependencies: @@ -2472,7 +2408,7 @@ snapshots: '@changesets/logger@0.1.1': dependencies: - picocolors: 1.1.0 + picocolors: 1.1.1 '@changesets/parse@0.4.0': dependencies: @@ -2494,7 +2430,7 @@ snapshots: '@changesets/types': 6.0.0 fs-extra: 7.0.1 p-filter: 2.1.0 - picocolors: 1.1.0 + picocolors: 1.1.1 '@changesets/should-skip-package@0.1.1': dependencies: @@ -2581,18 +2517,11 @@ snapshots: '@esbuild/win32-x64@0.21.5': optional: true - '@eslint-community/eslint-utils@4.4.0(eslint@9.9.1)': - dependencies: - eslint: 9.9.1 - eslint-visitor-keys: 3.4.3 - '@eslint-community/eslint-utils@4.4.1(eslint@9.9.1)': dependencies: eslint: 9.9.1 eslint-visitor-keys: 3.4.3 - '@eslint-community/regexpp@4.11.0': {} - '@eslint-community/regexpp@4.12.1': {} '@eslint/config-array@0.18.0': @@ -2700,9 +2629,9 @@ snapshots: '@rollup/pluginutils': 5.1.0(rollup@4.22.4) commondir: 1.0.1 estree-walker: 2.0.2 - fdir: 6.3.0(picomatch@4.0.2) + fdir: 6.4.3(picomatch@4.0.2) is-reference: 1.2.1 - magic-string: 0.30.11 + magic-string: 0.30.17 picomatch: 4.0.2 optionalDependencies: rollup: 4.22.4 @@ -2794,21 +2723,25 @@ snapshots: eslint-visitor-keys: 3.4.3 espree: 9.6.1 - '@sveltejs/eslint-config@8.1.0(@stylistic/eslint-plugin-js@1.8.0(eslint@9.9.1))(eslint-config-prettier@9.1.0(eslint@9.9.1))(eslint-plugin-n@17.9.0(eslint@9.9.1))(eslint-plugin-svelte@2.38.0(eslint@9.9.1)(svelte@packages+svelte))(eslint@9.9.1)(typescript-eslint@8.2.0(eslint@9.9.1)(typescript@5.5.4))(typescript@5.5.4)': + '@sveltejs/acorn-typescript@1.0.5(acorn@8.14.0)': + dependencies: + acorn: 8.14.0 + + '@sveltejs/eslint-config@8.1.0(@stylistic/eslint-plugin-js@1.8.0(eslint@9.9.1))(eslint-config-prettier@9.1.0(eslint@9.9.1))(eslint-plugin-n@17.16.1(eslint@9.9.1)(typescript@5.5.4))(eslint-plugin-svelte@2.38.0(eslint@9.9.1)(svelte@packages+svelte))(eslint@9.9.1)(typescript-eslint@8.26.0(eslint@9.9.1)(typescript@5.5.4))(typescript@5.5.4)': dependencies: '@stylistic/eslint-plugin-js': 1.8.0(eslint@9.9.1) eslint: 9.9.1 eslint-config-prettier: 9.1.0(eslint@9.9.1) - eslint-plugin-n: 17.9.0(eslint@9.9.1) + eslint-plugin-n: 17.16.1(eslint@9.9.1)(typescript@5.5.4) eslint-plugin-svelte: 2.38.0(eslint@9.9.1)(svelte@packages+svelte) - globals: 15.9.0 + globals: 15.14.0 typescript: 5.5.4 - typescript-eslint: 8.2.0(eslint@9.9.1)(typescript@5.5.4) + typescript-eslint: 8.26.0(eslint@9.9.1)(typescript@5.5.4) '@sveltejs/vite-plugin-svelte-inspector@3.0.0-next.2(@sveltejs/vite-plugin-svelte@4.0.0-next.6(svelte@packages+svelte)(vite@5.4.14(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)))(svelte@packages+svelte)(vite@5.4.14(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0))': dependencies: '@sveltejs/vite-plugin-svelte': 4.0.0-next.6(svelte@packages+svelte)(vite@5.4.14(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)) - debug: 4.3.6 + debug: 4.4.0 svelte: link:packages/svelte vite: 5.4.14(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0) transitivePeerDependencies: @@ -2817,10 +2750,10 @@ snapshots: '@sveltejs/vite-plugin-svelte@4.0.0-next.6(svelte@packages+svelte)(vite@5.4.14(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0))': dependencies: '@sveltejs/vite-plugin-svelte-inspector': 3.0.0-next.2(@sveltejs/vite-plugin-svelte@4.0.0-next.6(svelte@packages+svelte)(vite@5.4.14(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)))(svelte@packages+svelte)(vite@5.4.14(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)) - debug: 4.3.6 + debug: 4.4.0 deepmerge: 4.3.1 kleur: 4.1.5 - magic-string: 0.30.11 + magic-string: 0.30.17 svelte: link:packages/svelte vite: 5.4.14(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0) vitefu: 0.2.5(vite@5.4.14(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)) @@ -2857,99 +2790,95 @@ snapshots: '@types/semver@7.5.6': {} - '@typescript-eslint/eslint-plugin@8.2.0(@typescript-eslint/parser@8.2.0(eslint@9.9.1)(typescript@5.5.4))(eslint@9.9.1)(typescript@5.5.4)': + '@typescript-eslint/eslint-plugin@8.26.0(@typescript-eslint/parser@8.26.0(eslint@9.9.1)(typescript@5.5.4))(eslint@9.9.1)(typescript@5.5.4)': dependencies: - '@eslint-community/regexpp': 4.11.0 - '@typescript-eslint/parser': 8.2.0(eslint@9.9.1)(typescript@5.5.4) - '@typescript-eslint/scope-manager': 8.2.0 - '@typescript-eslint/type-utils': 8.2.0(eslint@9.9.1)(typescript@5.5.4) - '@typescript-eslint/utils': 8.2.0(eslint@9.9.1)(typescript@5.5.4) - '@typescript-eslint/visitor-keys': 8.2.0 + '@eslint-community/regexpp': 4.12.1 + '@typescript-eslint/parser': 8.26.0(eslint@9.9.1)(typescript@5.5.4) + '@typescript-eslint/scope-manager': 8.26.0 + '@typescript-eslint/type-utils': 8.26.0(eslint@9.9.1)(typescript@5.5.4) + '@typescript-eslint/utils': 8.26.0(eslint@9.9.1)(typescript@5.5.4) + '@typescript-eslint/visitor-keys': 8.26.0 eslint: 9.9.1 graphemer: 1.4.0 ignore: 5.3.2 natural-compare: 1.4.0 - ts-api-utils: 1.3.0(typescript@5.5.4) - optionalDependencies: + ts-api-utils: 2.0.1(typescript@5.5.4) typescript: 5.5.4 transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.2.0(eslint@9.9.1)(typescript@5.5.4)': + '@typescript-eslint/parser@8.26.0(eslint@9.9.1)(typescript@5.5.4)': dependencies: - '@typescript-eslint/scope-manager': 8.2.0 - '@typescript-eslint/types': 8.2.0 - '@typescript-eslint/typescript-estree': 8.2.0(typescript@5.5.4) - '@typescript-eslint/visitor-keys': 8.2.0 + '@typescript-eslint/scope-manager': 8.26.0 + '@typescript-eslint/types': 8.26.0 + '@typescript-eslint/typescript-estree': 8.26.0(typescript@5.5.4) + '@typescript-eslint/visitor-keys': 8.26.0 debug: 4.4.0 eslint: 9.9.1 - optionalDependencies: typescript: 5.5.4 transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@8.2.0': + '@typescript-eslint/scope-manager@8.26.0': dependencies: - '@typescript-eslint/types': 8.2.0 - '@typescript-eslint/visitor-keys': 8.2.0 + '@typescript-eslint/types': 8.26.0 + '@typescript-eslint/visitor-keys': 8.26.0 - '@typescript-eslint/type-utils@8.2.0(eslint@9.9.1)(typescript@5.5.4)': + '@typescript-eslint/type-utils@8.26.0(eslint@9.9.1)(typescript@5.5.4)': dependencies: - '@typescript-eslint/typescript-estree': 8.2.0(typescript@5.5.4) - '@typescript-eslint/utils': 8.2.0(eslint@9.9.1)(typescript@5.5.4) + '@typescript-eslint/typescript-estree': 8.26.0(typescript@5.5.4) + '@typescript-eslint/utils': 8.26.0(eslint@9.9.1)(typescript@5.5.4) debug: 4.4.0 - ts-api-utils: 1.3.0(typescript@5.5.4) - optionalDependencies: + eslint: 9.9.1 + ts-api-utils: 2.0.1(typescript@5.5.4) typescript: 5.5.4 transitivePeerDependencies: - - eslint - supports-color - '@typescript-eslint/types@8.2.0': {} + '@typescript-eslint/types@8.26.0': {} - '@typescript-eslint/typescript-estree@8.2.0(typescript@5.5.4)': + '@typescript-eslint/typescript-estree@8.26.0(typescript@5.5.4)': dependencies: - '@typescript-eslint/types': 8.2.0 - '@typescript-eslint/visitor-keys': 8.2.0 + '@typescript-eslint/types': 8.26.0 + '@typescript-eslint/visitor-keys': 8.26.0 debug: 4.4.0 - globby: 11.1.0 + fast-glob: 3.3.2 is-glob: 4.0.3 minimatch: 9.0.5 - semver: 7.6.3 - ts-api-utils: 1.3.0(typescript@5.5.4) - optionalDependencies: + semver: 7.7.1 + ts-api-utils: 2.0.1(typescript@5.5.4) typescript: 5.5.4 transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.2.0(eslint@9.9.1)(typescript@5.5.4)': + '@typescript-eslint/utils@8.26.0(eslint@9.9.1)(typescript@5.5.4)': dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@9.9.1) - '@typescript-eslint/scope-manager': 8.2.0 - '@typescript-eslint/types': 8.2.0 - '@typescript-eslint/typescript-estree': 8.2.0(typescript@5.5.4) + '@eslint-community/eslint-utils': 4.4.1(eslint@9.9.1) + '@typescript-eslint/scope-manager': 8.26.0 + '@typescript-eslint/types': 8.26.0 + '@typescript-eslint/typescript-estree': 8.26.0(typescript@5.5.4) eslint: 9.9.1 + typescript: 5.5.4 transitivePeerDependencies: - supports-color - - typescript - '@typescript-eslint/visitor-keys@8.2.0': + '@typescript-eslint/visitor-keys@8.26.0': dependencies: - '@typescript-eslint/types': 8.2.0 - eslint-visitor-keys: 3.4.3 + '@typescript-eslint/types': 8.26.0 + eslint-visitor-keys: 4.2.0 '@vitest/coverage-v8@2.0.5(vitest@2.1.9(@types/node@20.12.7)(jsdom@25.0.1)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0))': dependencies: '@ampproject/remapping': 2.3.0 '@bcoe/v8-coverage': 0.2.3 - debug: 4.3.6 + debug: 4.4.0 istanbul-lib-coverage: 3.2.2 istanbul-lib-report: 3.0.1 istanbul-lib-source-maps: 5.0.6 istanbul-reports: 3.1.7 - magic-string: 0.30.11 + magic-string: 0.30.17 magicast: 0.3.4 - std-env: 3.7.0 + std-env: 3.8.0 test-exclude: 7.0.1 tinyrainbow: 1.2.0 vitest: 2.1.9(@types/node@20.12.7)(jsdom@25.0.1)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0) @@ -2996,20 +2925,10 @@ snapshots: loupe: 3.1.3 tinyrainbow: 1.2.0 - acorn-jsx@5.3.2(acorn@8.12.1): - dependencies: - acorn: 8.12.1 - acorn-jsx@5.3.2(acorn@8.14.0): dependencies: acorn: 8.14.0 - acorn-typescript@1.4.13(acorn@8.12.1): - dependencies: - acorn: 8.12.1 - - acorn@8.12.1: {} - acorn@8.14.0: {} agent-base@7.1.1: @@ -3077,14 +2996,9 @@ snapshots: dependencies: balanced-match: 1.0.2 - braces@3.0.2: - dependencies: - fill-range: 7.0.1 - braces@3.0.3: dependencies: fill-range: 7.1.1 - optional: true buffer-from@1.1.2: {} @@ -3171,10 +3085,6 @@ snapshots: dataloader@1.4.0: {} - debug@4.3.6: - dependencies: - ms: 2.1.2 - debug@4.4.0: dependencies: ms: 2.1.3 @@ -3209,16 +3119,15 @@ snapshots: dotenv@16.3.2: {} - dts-buddy@0.5.3(typescript@5.5.4): + dts-buddy@0.5.5(typescript@5.5.4): dependencies: '@jridgewell/source-map': 0.3.6 '@jridgewell/sourcemap-codec': 1.5.0 - globrex: 0.1.2 kleur: 4.1.5 locate-character: 3.0.0 - magic-string: 0.30.11 + magic-string: 0.30.17 sade: 1.8.1 - tiny-glob: 0.2.9 + tinyglobby: 0.2.12 ts-api-utils: 1.3.0(typescript@5.5.4) typescript: 5.5.4 @@ -3290,9 +3199,10 @@ snapshots: eslint-plugin-lube@0.4.3: {} - eslint-plugin-n@17.9.0(eslint@9.9.1): + eslint-plugin-n@17.16.1(eslint@9.9.1)(typescript@5.5.4): dependencies: '@eslint-community/eslint-utils': 4.4.1(eslint@9.9.1) + '@typescript-eslint/utils': 8.26.0(eslint@9.9.1)(typescript@5.5.4) enhanced-resolve: 5.18.1 eslint: 9.9.1 eslint-plugin-es-x: 7.8.0(eslint@9.9.1) @@ -3301,6 +3211,10 @@ snapshots: ignore: 5.3.2 minimatch: 9.0.5 semver: 7.7.1 + ts-declaration-location: 1.0.5(typescript@5.5.4) + transitivePeerDependencies: + - supports-color + - typescript eslint-plugin-svelte@2.38.0(eslint@9.9.1)(svelte@packages+svelte): dependencies: @@ -3335,12 +3249,12 @@ snapshots: eslint-visitor-keys@3.4.3: {} - eslint-visitor-keys@4.0.0: {} + eslint-visitor-keys@4.2.0: {} eslint@9.9.1: dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@9.9.1) - '@eslint-community/regexpp': 4.11.0 + '@eslint-community/eslint-utils': 4.4.1(eslint@9.9.1) + '@eslint-community/regexpp': 4.12.1 '@eslint/config-array': 0.18.0 '@eslint/eslintrc': 3.1.0 '@eslint/js': 9.9.1 @@ -3350,10 +3264,10 @@ snapshots: ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.3 - debug: 4.3.6 + debug: 4.4.0 escape-string-regexp: 4.0.0 eslint-scope: 8.0.2 - eslint-visitor-keys: 4.0.0 + eslint-visitor-keys: 4.2.0 espree: 10.1.0 esquery: 1.5.0 esutils: 2.0.3 @@ -3380,9 +3294,9 @@ snapshots: espree@10.1.0: dependencies: - acorn: 8.12.1 - acorn-jsx: 5.3.2(acorn@8.12.1) - eslint-visitor-keys: 4.0.0 + acorn: 8.14.0 + acorn-jsx: 5.3.2(acorn@8.14.0) + eslint-visitor-keys: 4.2.0 espree@9.6.1: dependencies: @@ -3442,7 +3356,7 @@ snapshots: dependencies: reusify: 1.0.4 - fdir@6.3.0(picomatch@4.0.2): + fdir@6.4.3(picomatch@4.0.2): optionalDependencies: picomatch: 4.0.2 @@ -3450,14 +3364,9 @@ snapshots: dependencies: flat-cache: 4.0.1 - fill-range@7.0.1: - dependencies: - to-regex-range: 5.0.1 - fill-range@7.1.1: dependencies: to-regex-range: 5.0.1 - optional: true find-up@4.1.0: dependencies: @@ -3538,10 +3447,6 @@ snapshots: globals@15.14.0: {} - globals@15.9.0: {} - - globalyzer@0.1.0: {} - globby@11.1.0: dependencies: array-union: 2.1.0 @@ -3551,8 +3456,6 @@ snapshots: merge2: 1.4.1 slash: 3.0.0 - globrex@0.1.2: {} - graceful-fs@4.2.11: {} graphemer@1.4.0: {} @@ -3816,10 +3719,6 @@ snapshots: pseudomap: 1.0.2 yallist: 2.1.2 - magic-string@0.30.11: - dependencies: - '@jridgewell/sourcemap-codec': 1.5.0 - magic-string@0.30.17: dependencies: '@jridgewell/sourcemap-codec': 1.5.0 @@ -3832,13 +3731,13 @@ snapshots: make-dir@4.0.0: dependencies: - semver: 7.6.3 + semver: 7.7.1 merge2@1.4.1: {} micromatch@4.0.5: dependencies: - braces: 3.0.2 + braces: 3.0.3 picomatch: 2.3.1 mime-db@1.52.0: {} @@ -3847,6 +3746,10 @@ snapshots: dependencies: mime-db: 1.52.0 + minimatch@10.0.1: + dependencies: + brace-expansion: 2.0.1 + minimatch@3.1.2: dependencies: brace-expansion: 1.1.11 @@ -3861,12 +3764,8 @@ snapshots: mrmime@2.0.0: {} - ms@2.1.2: {} - ms@2.1.3: {} - nanoid@3.3.7: {} - nanoid@3.3.8: {} natural-compare@1.4.0: {} @@ -3955,8 +3854,6 @@ snapshots: perfect-debounce@1.0.0: {} - picocolors@1.1.0: {} - picocolors@1.1.1: {} picomatch@2.3.1: {} @@ -3998,12 +3895,6 @@ snapshots: cssesc: 3.0.0 util-deprecate: 1.0.2 - postcss@8.4.47: - dependencies: - nanoid: 3.3.7 - picocolors: 1.1.0 - source-map-js: 1.2.1 - postcss@8.5.1: dependencies: nanoid: 3.3.8 @@ -4110,8 +4001,6 @@ snapshots: dependencies: xmlchars: 2.2.0 - semver@7.6.3: {} - semver@7.7.1: {} serialize-javascript@6.0.2: @@ -4166,8 +4055,6 @@ snapshots: stackback@0.0.2: {} - std-env@3.7.0: {} - std-env@3.8.0: {} string-width@4.2.3: @@ -4219,7 +4106,7 @@ snapshots: terser@5.27.0: dependencies: '@jridgewell/source-map': 0.3.6 - acorn: 8.12.1 + acorn: 8.14.0 commander: 2.20.3 source-map-support: 0.5.21 @@ -4231,15 +4118,15 @@ snapshots: text-table@0.2.0: {} - tiny-glob@0.2.9: - dependencies: - globalyzer: 0.1.0 - globrex: 0.1.2 - tinybench@2.9.0: {} tinyexec@0.3.2: {} + tinyglobby@0.2.12: + dependencies: + fdir: 6.4.3(picomatch@4.0.2) + picomatch: 4.0.2 + tinypool@1.0.2: {} tinyrainbow@1.2.0: {} @@ -4282,19 +4169,27 @@ snapshots: dependencies: typescript: 5.5.4 + ts-api-utils@2.0.1(typescript@5.5.4): + dependencies: + typescript: 5.5.4 + + ts-declaration-location@1.0.5(typescript@5.5.4): + dependencies: + minimatch: 10.0.1 + typescript: 5.5.4 + type-check@0.4.0: dependencies: prelude-ls: 1.2.1 - typescript-eslint@8.2.0(eslint@9.9.1)(typescript@5.5.4): + typescript-eslint@8.26.0(eslint@9.9.1)(typescript@5.5.4): dependencies: - '@typescript-eslint/eslint-plugin': 8.2.0(@typescript-eslint/parser@8.2.0(eslint@9.9.1)(typescript@5.5.4))(eslint@9.9.1)(typescript@5.5.4) - '@typescript-eslint/parser': 8.2.0(eslint@9.9.1)(typescript@5.5.4) - '@typescript-eslint/utils': 8.2.0(eslint@9.9.1)(typescript@5.5.4) - optionalDependencies: + '@typescript-eslint/eslint-plugin': 8.26.0(@typescript-eslint/parser@8.26.0(eslint@9.9.1)(typescript@5.5.4))(eslint@9.9.1)(typescript@5.5.4) + '@typescript-eslint/parser': 8.26.0(eslint@9.9.1)(typescript@5.5.4) + '@typescript-eslint/utils': 8.26.0(eslint@9.9.1)(typescript@5.5.4) + eslint: 9.9.1 typescript: 5.5.4 transitivePeerDependencies: - - eslint - supports-color typescript@5.5.4: {} @@ -4335,12 +4230,12 @@ snapshots: dependencies: '@antfu/utils': 0.7.8 '@rollup/pluginutils': 5.1.0(rollup@4.22.4) - debug: 4.3.6 + debug: 4.4.0 error-stack-parser-es: 0.1.1 fs-extra: 11.2.0 open: 10.1.0 perfect-debounce: 1.0.0 - picocolors: 1.1.0 + picocolors: 1.1.1 sirv: 2.0.4 vite: 5.4.14(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0) transitivePeerDependencies: @@ -4350,7 +4245,7 @@ snapshots: vite@5.4.14(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0): dependencies: esbuild: 0.21.5 - postcss: 8.4.47 + postcss: 8.5.1 rollup: 4.22.4 optionalDependencies: '@types/node': 20.12.7