Merge branch 'main' into svelte-html

svelte-html
Simon Holthausen 9 months ago
commit 0c1c8b992b

@ -1,5 +0,0 @@
---
'svelte': patch
---
fix: correctly remove unused selectors in middle of selector lists

@ -36,12 +36,7 @@ let todos = $state([
...modifying an individual todo's property will trigger updates to anything in your UI that depends on that specific property: ...modifying an individual todo's property will trigger updates to anything in your UI that depends on that specific property:
```js ```js
// @filename: ambient.d.ts let todos = [{ done: false, text: 'add more todos' }];
declare global {
const todos: Array<{ done: boolean, text: string }>
}
// @filename: index.js
// ---cut--- // ---cut---
todos[0].done = !todos[0].done; todos[0].done = !todos[0].done;
``` ```
@ -64,6 +59,17 @@ todos.push({
> [!NOTE] When you update properties of proxies, the original object is _not_ mutated. > [!NOTE] When you update properties of proxies, the original object is _not_ mutated.
Note that if you destructure a reactive value, the references are not reactive — as in normal JavaScript, they are evaluated at the point of destructuring:
```js
let todos = [{ done: false, text: 'add more todos' }];
// ---cut---
let { done, text } = todos[0];
// this will not affect the value of `done`
todos[0].done = !todos[0].done;
```
### Classes ### Classes
You can also use `$state` in class fields (whether public or private): You can also use `$state` in class fields (whether public or private):
@ -85,7 +91,42 @@ class Todo {
} }
``` ```
> [!NOTE] The compiler transforms `done` and `text` into `get`/`set` methods on the class prototype referencing private fields. > [!NOTE] The compiler transforms `done` and `text` into `get`/`set` methods on the class prototype referencing private fields. This means the properties are not enumerable.
When calling methods in JavaScript, the value of [`this`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/this) matters. This won't work, because `this` inside the `reset` method will be the `<button>` rather than the `Todo`:
```svelte
<button onclick={todo.reset}>
reset
</button>
```
You can either use an inline function...
```svelte
<button onclick=+++{() => todo.reset()}>+++
reset
</button>
```
...or use an arrow function in the class definition:
```js
// @errors: 7006 2554
class Todo {
done = $state(false);
text = $state();
constructor(text) {
this.text = text;
}
+++reset = () => {+++
this.text = '';
this.done = false;
}
}
```
## `$state.raw` ## `$state.raw`
@ -127,3 +168,90 @@ To take a static snapshot of a deeply reactive `$state` proxy, use `$state.snaps
``` ```
This is handy when you want to pass some state to an external library or API that doesn't expect a proxy, such as `structuredClone`. This is handy when you want to pass some state to an external library or API that doesn't expect a proxy, such as `structuredClone`.
## Passing state into functions
JavaScript is a _pass-by-value_ language — when you call a function, the arguments are the _values_ rather than the _variables_. In other words:
```js
/// file: index.js
// @filename: index.js
// ---cut---
/**
* @param {number} a
* @param {number} b
*/
function add(a, b) {
return a + b;
}
let a = 1;
let b = 2;
let total = add(a, b);
console.log(total); // 3
a = 3;
b = 4;
console.log(total); // still 3!
```
If `add` wanted to have access to the _current_ values of `a` and `b`, and to return the current `total` value, you would need to use functions instead:
```js
/// file: index.js
// @filename: index.js
// ---cut---
/**
* @param {() => number} getA
* @param {() => number} getB
*/
function add(+++getA, getB+++) {
return +++() => getA() + getB()+++;
}
let a = 1;
let b = 2;
let total = add+++(() => a, () => b)+++;
console.log(+++total()+++); // 3
a = 3;
b = 4;
console.log(+++total()+++); // 7
```
State in Svelte is no different — when you reference something declared with the `$state` rune...
```js
let a = +++$state(1)+++;
let b = +++$state(2)+++;
```
...you're accessing its _current value_.
Note that 'functions' is broad — it encompasses properties of proxies and [`get`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/get)/[`set`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/set) properties...
```js
/// file: index.js
// @filename: index.js
// ---cut---
/**
* @param {{ a: number, b: number }} input
*/
function add(input) {
return {
get value() {
return input.a + input.b;
}
};
}
let input = $state({ a: 1, b: 2 });
let total = add(input);
console.log(total.value); // 3
input.a = 3;
input.b = 4;
console.log(total.value); // 7
```
...though if you find yourself writing code like that, consider using [classes](#Classes) instead.

@ -74,6 +74,30 @@ You can freely use destructuring and rest patterns in each blocks.
{/each} {/each}
``` ```
## Each blocks without an item
```svelte
<!--- copy: false --->
{#each expression}...{/each}
```
```svelte
<!--- copy: false --->
{#each expression, index}...{/each}
```
In case you just want to render something `n` times, you can omit the `as` part ([demo](/playground/untitled#H4sIAAAAAAAAE3WR0W7CMAxFf8XKNAk0WsSeUEaRpn3Guoc0MbQiJFHiMlDVf18SOrZJ48259_jaVgZmxBEZZ28thgCNFV6xBdt1GgPj7wOji0t2EqI-wa_OleGEmpLWiID_6dIaQkMxhm1UdwKpRQhVzWSaVORJNdvWpqbhAYVsYQCNZk8thzWMC_DCHMZk3wPSThNQ088I3mghD9UwSwHwlLE5PMIzVFUFq3G7WUZ2OyUvU3JOuZU332wCXTRmtPy1NgzXZtUFp8WFw9536uWqpbIgPEaDsJBW90cTOHh0KGi2XsBq5-cT6-3nPauxXqHnsHJnCFZ3CvJVkyuCQ0mFF9TZyCQ162WGvteLKfG197Y3iv_pz_fmS68Hxt8iPBPj5HscP8YvCNX7uhYCAAA=)):
```svelte
<div class="chess-board">
{#each { length: 8 }, rank}
{#each { length: 8 }, file}
<div class:black={(rank + file) % 2 === 1}></div>
{/each}
{/each}
</div>
```
## Else blocks ## Else blocks
```svelte ```svelte

@ -246,6 +246,23 @@ We can tighten things up further by declaring a generic, so that `data` and `row
</script> </script>
``` ```
## Exporting snippets
Snippets declared at the top level of a `.svelte` file can be exported from a `<script module>` for use in other components, provided they don't reference any declarations in a non-module `<script>` (whether directly or indirectly, via other snippets) ([demo](/playground/untitled#H4sIAAAAAAAAE3WPwY7CMAxEf8UyB1hRgdhjl13Bga8gHFJipEqtGyUGFUX5dxJUtEB3b9bYM_MckHVLWOKut50TMuC5tpbEY4GnuiGP5T6gXG0-ykLSB8vW2oW_UCNZq7Snv_Rjx0Kc4kpc-6OrrfwoVlK3uQ4CaGMgwsl1LUwXy0f54J9-KV4vf20cNo7YkMu22aqAz4-oOLUI9YKluDPF4h_at-hX5PFyzA1tZ84N3fGpf8YfUU6GvDumLqDKmEqCjjCHUEX4hqDTWCU5PJ6Or38c4g1cPu9tnAEAAA==)):
```svelte
<script module>
export { add };
</script>
{#snippet add(a, b)}
{a} + {b} = {a + b}
{/snippet}
```
> [!NOTE]
> This requires Svelte 5.5.0 or newer
## Programmatic snippets ## Programmatic snippets
Snippets can be created programmatically with the [`createRawSnippet`](svelte#createRawSnippet) API. This is intended for advanced use cases. Snippets can be created programmatically with the [`createRawSnippet`](svelte#createRawSnippet) API. This is intended for advanced use cases.

@ -53,6 +53,22 @@ In the case of a numeric input (`type="number"` or `type="range"`), the value wi
If the input is empty or invalid (in the case of `type="number"`), the value is `undefined`. If the input is empty or invalid (in the case of `type="number"`), the value is `undefined`.
Since 5.6.0, if an `<input>` has a `defaultValue` and is part of a form, it will revert to that value instead of the empty string when the form is reset. Note that for the initial render the value of the binding takes precedence unless it is `null` or `undefined`.
```svelte
<script>
let value = $state('');
</script>
<form>
<input bind:value defaultValue="not the empty string">
<input type="reset" value="Reset">
</form>
```
> [!NOTE]
> Use reset buttons sparingly, and ensure that users won't accidentally click them while trying to submit the form.
## `<input bind:checked>` ## `<input bind:checked>`
Checkbox and radio inputs can be bound with `bind:checked`: Checkbox and radio inputs can be bound with `bind:checked`:
@ -64,16 +80,29 @@ Checkbox and radio inputs can be bound with `bind:checked`:
</label> </label>
``` ```
Since 5.6.0, if an `<input>` has a `defaultChecked` attribute and is part of a form, it will revert to that value instead of `false` when the form is reset. Note that for the initial render the value of the binding takes precedence unless it is `null` or `undefined`.
```svelte
<script>
let checked = $state(true);
</script>
<form>
<input type="checkbox" bind:checked defaultChecked={true}>
<input type="reset" value="Reset">
</form>
```
## `<input bind:group>` ## `<input bind:group>`
Inputs that work together can use `bind:group`. Inputs that work together can use `bind:group`.
```svelte ```svelte
<script> <script>
let tortilla = 'Plain'; let tortilla = $state('Plain');
/** @type {Array<string>} */ /** @type {Array<string>} */
let fillings = []; let fillings = $state([]);
</script> </script>
<!-- grouped radio inputs are mutually exclusive --> <!-- grouped radio inputs are mutually exclusive -->
@ -146,6 +175,16 @@ When the value of an `<option>` matches its text content, the attribute can be o
</select> </select>
``` ```
You can give the `<select>` a default value by adding a `selected` attribute to the`<option>` (or options, in the case of `<select multiple>`) that should be initially selected. If the `<select>` is part of a form, it will revert to that selection when the form is reset. Note that for the initial render the value of the binding takes precedence if it's not `undefined`.
```svelte
<select bind:value={selected}>
<option value={a}>a</option>
<option value={b} selected>b</option>
<option value={c}>c</option>
</select>
```
## `<audio>` ## `<audio>`
`<audio>` elements have their own set of bindings — five two-way ones... `<audio>` elements have their own set of bindings — five two-way ones...

@ -0,0 +1,82 @@
---
title: <svelte:boundary>
---
```svelte
<svelte:boundary onerror={handler}>...</svelte:boundary>
```
> [!NOTE]
> This feature was added in 5.3.0
Boundaries allow you to guard against errors in part of your app from breaking the app as a whole, and to recover from those errors.
If an error occurs while rendering or updating the children of a `<svelte:boundary>`, or running any [`$effect`]($effect) functions contained therein, the contents will be removed.
Errors occurring outside the rendering process (for example, in event handlers) are _not_ caught by error boundaries.
## Properties
For the boundary to do anything, one or both of `failed` and `onerror` must be provided.
### `failed`
If a `failed` snippet is provided, it will be rendered with the error that was thrown, and a `reset` function that recreates the contents ([demo](/playground/hello-world#H4sIAAAAAAAAE3VRy26DMBD8lS2tFCIh6JkAUlWp39Cq9EBg06CAbdlLArL87zWGKk8ORnhmd3ZnrD1WtOjFXqKO2BDGW96xqpBD5gXerm5QefG39mgQY9EIWHxueRMinLosti0UPsJLzggZKTeilLWgLGc51a3gkuCjKQ7DO7cXZotgJ3kLqzC6hmex1SZnSXTWYHcrj8LJjWTk0PHoZ8VqIdCOKayPykcpuQxAokJaG1dGybYj4gw4K5u6PKTasSbjXKgnIDlA8VvUdo-pzonraBY2bsH7HAl78mKSHZpgIcuHjq9jXSpZSLixRlveKYQUXhQVhL6GPobXAAb7BbNeyvNUs4qfRg3OnELLj5hqH9eQZqCnoBwR9lYcQxuVXeBzc8kMF8yXY4yNJ5oGiUzP_aaf_waTRGJib5_Ad3P_vbCuaYxzeNpbU0eUMPAOKh7Yw1YErgtoXyuYlPLzc10_xo_5A91zkQL_AgAA)):
```svelte
<svelte:boundary>
<FlakyComponent />
{#snippet failed(error, reset)}
<button onclick={reset}>oops! try again</button>
{/snippet}
</svelte:boundary>
```
> [!NOTE]
> As with [snippets passed to components](snippet#Passing-snippets-to-components), the `failed` snippet can be passed explicitly as a property...
>
> ```svelte
> <svelte:boundary {failed}>...</svelte:boundary>
> ```
>
> ...or implicitly by declaring it directly inside the boundary, as in the example above.
### `onerror`
If an `onerror` function is provided, it will be called with the same two `error` and `reset` arguments. This is useful for tracking the error with an error reporting service...
```svelte
<svelte:boundary onerror={(e) => report(e)}>
...
</svelte:boundary>
```
...or using `error` and `reset` outside the boundary itself:
```svelte
<script>
let error = $state(null);
let reset = $state(() => {});
function onerror(e, r) {
error = e;
reset = r;
}
</script>
<svelte:boundary {onerror}>
<FlakyComponent />
</svelte:boundary>
{#if error}
<button onclick={() => {
error = null;
reset();
}}>
oops! try again
</button>
{/if}
```
If an error occurs inside the `onerror` function (or if you rethrow the error), it will be handled by a parent boundary if such exists.

@ -40,26 +40,26 @@ If you want to use one of these features, you need to setup up a `script` prepro
To use non-type-only TypeScript features within Svelte components, you need to add a preprocessor that will turn TypeScript into JavaScript. To use non-type-only TypeScript features within Svelte components, you need to add a preprocessor that will turn TypeScript into JavaScript.
### Using SvelteKit or Vite
The easiest way to get started is scaffolding a new SvelteKit project by typing `npx sv create`, following the prompts and choosing the TypeScript option.
```ts ```ts
/// file: svelte.config.js /// file: svelte.config.js
// @noErrors // @noErrors
import { vitePreprocess } from '@sveltejs/kit/vite'; import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
const config = { const config = {
preprocess: vitePreprocess() // Note the additional `{ script: true }`
preprocess: vitePreprocess({ script: true })
}; };
export default config; export default config;
``` ```
If you don't need or want all the features SvelteKit has to offer, you can scaffold a Svelte-flavoured Vite project instead by typing `npm create vite@latest` and selecting the `svelte-ts` option. ### Using SvelteKit or Vite
The easiest way to get started is scaffolding a new SvelteKit project by typing `npx sv create`, following the prompts and choosing the TypeScript option.
```ts ```ts
/// file: svelte.config.js /// file: svelte.config.js
// @noErrors
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
const config = { const config = {
@ -69,6 +69,8 @@ const config = {
export default config; export default config;
``` ```
If you don't need or want all the features SvelteKit has to offer, you can scaffold a Svelte-flavoured Vite project instead by typing `npm create vite@latest` and selecting the `svelte-ts` option.
In both cases, a `svelte.config.js` with `vitePreprocess` will be added. Vite/SvelteKit will read from this config file. In both cases, a `svelte.config.js` with `vitePreprocess` will be added. Vite/SvelteKit will read from this config file.
### Other build tools ### Other build tools
@ -77,6 +79,14 @@ If you're using tools like Rollup or Webpack instead, install their respective S
> [!NOTE] If you're starting a new project, we recommend using SvelteKit or Vite instead > [!NOTE] If you're starting a new project, we recommend using SvelteKit or Vite instead
## tsconfig.json settings
When using TypeScript, make sure your `tsconfig.json` is setup correctly.
- Use a [`target`](https://www.typescriptlang.org/tsconfig/#target) of at least `ES2022`, or a `target` of at least `ES2015` alongside [`useDefineForClassFields`](https://www.typescriptlang.org/tsconfig/#useDefineForClassFields). This ensures that rune declarations on class fields are not messed with, which would break the Svelte compiler
- Set [`verbatimModuleSyntax`](https://www.typescriptlang.org/tsconfig/#verbatimModuleSyntax) to `true` so that imports are left as-is
- Set [`isolatedModules`](https://www.typescriptlang.org/tsconfig/#isolatedModules) to `true` so that each file is looked at in isolation. TypeScript has a few features which require cross-file analysis and compilation, which the Svelte compiler and tooling like Vite don't do.
## Typing `$props` ## Typing `$props`
Type `$props` just like a regular object with certain properties. Type `$props` just like a regular object with certain properties.

@ -823,6 +823,8 @@ The `foreign` namespace was only useful for Svelte Native, which we're planning
`afterUpdate` callbacks in a parent component will now run after `afterUpdate` callbacks in any child components. `afterUpdate` callbacks in a parent component will now run after `afterUpdate` callbacks in any child components.
`beforeUpdate/afterUpdate` no longer run when the component contains a `<slot>` and its content is updated.
Both functions are disallowed in runes mode — use `$effect.pre(...)` and `$effect(...)` instead. Both functions are disallowed in runes mode — use `$effect.pre(...)` and `$effect(...)` instead.
### `contenteditable` behavior change ### `contenteditable` behavior change

@ -102,6 +102,12 @@ If you need hash-based routing on the client side, check out [svelte-spa-router]
You can see a [community-maintained list of routers on sveltesociety.dev](https://sveltesociety.dev/packages?category=routers). You can see a [community-maintained list of routers on sveltesociety.dev](https://sveltesociety.dev/packages?category=routers).
## How do I write a mobile app with Svelte?
While most mobile apps are written without using JavaScript, if you'd like to leverage your existing Svelte components and knowledge of Svelte when building mobile apps, you can turn a [SvelteKit SPA](https://kit.svelte.dev/docs/single-page-apps) into a mobile app with [Tauri](https://v2.tauri.app/start/frontend/sveltekit/) or [Capacitor](https://capacitorjs.com/solution/svelte). Mobile features like the camera, geolocation, and push notifications are available via plugins for both platforms.
Svelte Native was an option available for Svelte 4, but note that Svelte 5 does not currently support it. Svelte Native lets you write NativeScript apps using Svelte components that contain [NativeScript UI components](https://docs.nativescript.org/ui/) rather than DOM elements, which may be familiar for users coming from React Native.
## Can I tell Svelte not to remove my unused styles? ## Can I tell Svelte not to remove my unused styles?
No. Svelte removes the styles from the component and warns you about them in order to prevent issues that would otherwise arise. No. Svelte removes the styles from the component and warns you about them in order to prevent issues that would otherwise arise.

@ -1,5 +1,39 @@
<!-- This file is generated by scripts/process-messages/index.js. Do not edit! --> <!-- This file is generated by scripts/process-messages/index.js. Do not edit! -->
### assignment_value_stale
```
Assignment to `%property%` property (%location%) will evaluate to the right-hand side, not the value of `%property%` following the assignment. This may result in unexpected behaviour.
```
Given a case like this...
```svelte
<script>
let object = $state({ array: null });
function add() {
(object.array ??= []).push(object.array.length);
}
</script>
<button onclick={add}>add</button>
<p>items: {JSON.stringify(object.items)}</p>
```
...the array being pushed to when the button is first clicked is the `[]` on the right-hand side of the assignment, but the resulting value of `object.array` is an empty state proxy. As a result, the pushed value will be discarded.
You can fix this by separating it into two statements:
```js
let object = { array: [0] };
// ---cut---
function add() {
object.array ??= [];
object.array.push(object.array.length);
}
```
### binding_property_non_reactive ### binding_property_non_reactive
``` ```
@ -86,6 +120,46 @@ Mutating a value outside the component that created it is strongly discouraged.
%component% mutated a value owned by %owner%. This is strongly discouraged. Consider passing values to child components with `bind:`, or use a callback instead %component% mutated a value owned by %owner%. This is strongly discouraged. Consider passing values to child components with `bind:`, or use a callback instead
``` ```
### reactive_declaration_non_reactive_property
```
A `$:` statement (%location%) read reactive state that was not visible to the compiler. Updates to this state will not cause the statement to re-run. The behaviour of this code will change if you migrate it to runes mode
```
In legacy mode, a `$:` [reactive statement](https://svelte.dev/docs/svelte/legacy-reactive-assignments) re-runs when the state it _references_ changes. This is determined at compile time, by analysing the code.
In runes mode, effects and deriveds re-run when there are changes to the values that are read during the function's _execution_.
Often, the result is the same — for example these can be considered equivalent:
```js
let a = 1, b = 2, sum = 3;
// ---cut---
$: sum = a + b;
```
```js
let a = 1, b = 2;
// ---cut---
const sum = $derived(a + b);
```
In some cases — such as the one that triggered the above warning — they are _not_ the same:
```js
let a = 1, b = 2, sum = 3;
// ---cut---
const add = () => a + b;
// the compiler can't 'see' that `sum` depends on `a` and `b`, but
// they _would_ be read while executing the `$derived` version
$: sum = add();
```
Similarly, reactive properties of [deep state](https://svelte.dev/docs/svelte/$state#Deep-state) are not visible to the compiler. As such, changes to these properties will cause effects and deriveds to re-run but will _not_ cause `$:` statements to re-run.
When you [migrate this component](https://svelte.dev/docs/svelte/v5-migration-guide) to runes mode, the behaviour will change accordingly.
### state_proxy_equality_mismatch ### state_proxy_equality_mismatch
``` ```

@ -400,6 +400,12 @@ Expected token %token%
Expected whitespace Expected whitespace
``` ```
### export_undefined
```
`%name%` is not defined
```
### global_reference_invalid ### global_reference_invalid
``` ```
@ -481,12 +487,12 @@ A component cannot have a default export
### node_invalid_placement ### node_invalid_placement
``` ```
%thing% is invalid inside `<%parent%>` %message%. The browser will 'repair' the HTML (by moving, removing, or inserting elements) which breaks Svelte's assumptions about the structure of your components.
``` ```
HTML restricts where certain elements can appear. In case of a violation the browser will 'repair' the HTML in a way that breaks Svelte's assumptions about the structure of your components. Some examples: HTML restricts where certain elements can appear. In case of a violation the browser will 'repair' the HTML in a way that breaks Svelte's assumptions about the structure of your components. Some examples:
- `<p>hello <div>world</div></p>` will result in `<p>hello </p><div>world</div><p></p>` for example (the `<div>` autoclosed the `<p>` because `<p>` cannot contain block-level elements) - `<p>hello <div>world</div></p>` will result in `<p>hello </p><div>world</div><p></p>` (the `<div>` autoclosed the `<p>` because `<p>` cannot contain block-level elements)
- `<option><div>option a</div></option>` will result in `<option>option a</option>` (the `<div>` is removed) - `<option><div>option a</div></option>` will result in `<option>option a</option>` (the `<div>` is removed)
- `<table><tr><td>cell</td></tr></table>` will result in `<table><tbody><tr><td>cell</td></tr></tbody></table>` (a `<tbody>` is auto-inserted) - `<table><tr><td>cell</td></tr></table>` will result in `<table><tbody><tr><td>cell</td></tr></tbody></table>` (a `<tbody>` is auto-inserted)
@ -694,6 +700,30 @@ Cannot use `<slot>` syntax and `{@render ...}` tags in the same component. Migra
Cannot use explicit children snippet at the same time as implicit children content. Remove either the non-whitespace content or the children snippet block Cannot use explicit children snippet at the same time as implicit children content. Remove either the non-whitespace content or the children snippet block
``` ```
### snippet_invalid_export
```
An exported snippet can only reference things declared in a `<script module>`, or other exportable snippets
```
It's possible to export a snippet from a `<script module>` block, but only if it doesn't reference anything defined inside a non-module-level `<script>`. For example you can't do this...
```svelte
<script module>
export { greeting };
</script>
<script>
let message = 'hello';
</script>
{#snippet greeting(name)}
<p>{message} {name}!</p>
{/snippet}
```
...because `greeting` references `message`, which is defined in the second `<script>`.
### snippet_invalid_rest_parameter ### snippet_invalid_rest_parameter
``` ```
@ -762,6 +792,18 @@ A component can have a single top-level `<style>` element
`<svelte:body>` does not support non-event attributes or spread attributes `<svelte:body>` does not support non-event attributes or spread attributes
``` ```
### svelte_boundary_invalid_attribute
```
Valid attributes on `<svelte:boundary>` are `onerror` and `failed`
```
### svelte_boundary_invalid_attribute_value
```
Attribute value must be a non-string expression
```
### svelte_component_invalid_this ### svelte_component_invalid_this
``` ```

@ -643,12 +643,12 @@ Svelte 5 components are no longer classes. Instantiate them using `mount` or `hy
### node_invalid_placement_ssr ### node_invalid_placement_ssr
``` ```
%thing% is invalid inside `<%parent%>`. When rendering this component on the server, the resulting HTML will be modified by the browser, likely resulting in a `hydration_mismatch` warning %message%. When rendering this component on the server, the resulting HTML will be modified by the browser (by moving, removing, or inserting elements), likely resulting in a `hydration_mismatch` warning
``` ```
HTML restricts where certain elements can appear. In case of a violation the browser will 'repair' the HTML in a way that breaks Svelte's assumptions about the structure of your components. Some examples: HTML restricts where certain elements can appear. In case of a violation the browser will 'repair' the HTML in a way that breaks Svelte's assumptions about the structure of your components. Some examples:
- `<p>hello <div>world</div></p>` will result in `<p>hello </p><div>world</div><p></p>` for example (the `<div>` autoclosed the `<p>` because `<p>` cannot contain block-level elements) - `<p>hello <div>world</div></p>` will result in `<p>hello </p><div>world</div><p></p>` (the `<div>` autoclosed the `<p>` because `<p>` cannot contain block-level elements)
- `<option><div>option a</div></option>` will result in `<option>option a</option>` (the `<div>` is removed) - `<option><div>option a</div></option>` will result in `<option>option a</option>` (the `<div>` is removed)
- `<table><tr><td>cell</td></tr></table>` will result in `<table><tbody><tr><td>cell</td></tr></tbody></table>` (a `<tbody>` is auto-inserted) - `<table><tr><td>cell</td></tr></table>` will result in `<table><tbody><tr><td>cell</td></tr></tbody></table>` (a `<tbody>` is auto-inserted)
@ -726,12 +726,6 @@ Reactive declarations only exist at the top level of the instance script
Reassignments of module-level declarations will not cause reactive statements to update Reassignments of module-level declarations will not cause reactive statements to update
``` ```
### reactive_declaration_non_reactive_property
```
Properties of objects and arrays are not reactive unless in runes mode. Changes to this property will not cause the reactive statement to update
```
### script_context_deprecated ### script_context_deprecated
``` ```

@ -74,39 +74,45 @@ If no slotted content is provided, a component can define fallback content by pu
Slots can be rendered zero or more times and can pass values _back_ to the parent using props. The parent exposes the values to the slot template using the `let:` directive. Slots can be rendered zero or more times and can pass values _back_ to the parent using props. The parent exposes the values to the slot template using the `let:` directive.
The usual shorthand rules apply — `let:item` is equivalent to `let:item={item}`, and `<slot {item}>` is equivalent to `<slot item={item}>`.
```svelte ```svelte
<!-- FancyList.svelte --> <!--- file: FancyList.svelte --->
<ul> <ul>
{#each items as item} {#each items as data}
<li class="fancy"> <li class="fancy">
<slot prop={item} /> <!-- 'item' here... -->
<slot item={process(data)} />
</li> </li>
{/each} {/each}
</ul> </ul>
```
<!-- App.svelte --> ```svelte
<FancyList {items} let:prop={thing}> <!--- file: App.svelte --->
<div>{thing.text}</div> <!-- ...corresponds to 'item' here: -->
<FancyList {items} let:item={processed}>
<div>{processed.text}</div>
</FancyList> </FancyList>
``` ```
The usual shorthand rules apply — `let:item` is equivalent to `let:item={item}`, and `<slot {item}>` is equivalent to `<slot item={item}>`.
Named slots can also expose values. The `let:` directive goes on the element with the `slot` attribute. Named slots can also expose values. The `let:` directive goes on the element with the `slot` attribute.
```svelte ```svelte
<!-- FancyList.svelte --> <!--- file: FancyList.svelte --->
<ul> <ul>
{#each items as item} {#each items as item}
<li class="fancy"> <li class="fancy">
<slot name="item" {item} /> <slot name="item" item={process(data)} />
</li> </li>
{/each} {/each}
</ul> </ul>
<slot name="footer" /> <slot name="footer" />
```
<!-- App.svelte --> ```svelte
<!--- file: App.svelte --->
<FancyList {items}> <FancyList {items}>
<div slot="item" let:item>{item.text}</div> <div slot="item" let:item>{item.text}</div>
<p slot="footer">Copyright (c) 2019 Svelte Industries</p> <p slot="footer">Copyright (c) 2019 Svelte Industries</p>

@ -36,7 +36,7 @@
"@vitest/coverage-v8": "^2.0.5", "@vitest/coverage-v8": "^2.0.5",
"eslint": "^9.9.1", "eslint": "^9.9.1",
"eslint-plugin-lube": "^0.4.3", "eslint-plugin-lube": "^0.4.3",
"jsdom": "25.0.0", "jsdom": "25.0.1",
"playwright": "^1.46.1", "playwright": "^1.46.1",
"prettier": "^3.2.4", "prettier": "^3.2.4",
"prettier-plugin-svelte": "^3.1.2", "prettier-plugin-svelte": "^3.1.2",

@ -1,5 +1,143 @@
# svelte # svelte
## 5.7.1
### Patch Changes
- fix: ensure bindings always take precedence over spreads ([#14575](https://github.com/sveltejs/svelte/pull/14575))
## 5.7.0
### Minor Changes
- feat: add `createSubscriber` function for creating reactive values that depend on subscriptions ([#14422](https://github.com/sveltejs/svelte/pull/14422))
- feat: add reactive `MediaQuery` class, and a `prefersReducedMotion` class instance ([#14422](https://github.com/sveltejs/svelte/pull/14422))
### Patch Changes
- fix: treat `undefined` and `null` the same for the initial input value ([#14562](https://github.com/sveltejs/svelte/pull/14562))
## 5.6.2
### Patch Changes
- chore: make if blocks tree-shakable ([#14549](https://github.com/sveltejs/svelte/pull/14549))
## 5.6.1
### Patch Changes
- fix: handle static form values in combination with default values ([#14555](https://github.com/sveltejs/svelte/pull/14555))
## 5.6.0
### Minor Changes
- feat: support `defaultValue/defaultChecked` for inputs ([#14289](https://github.com/sveltejs/svelte/pull/14289))
## 5.5.4
### Patch Changes
- fix: better error messages for invalid HTML trees ([#14445](https://github.com/sveltejs/svelte/pull/14445))
- fix: remove spreaded event handlers when they become nullish ([#14546](https://github.com/sveltejs/svelte/pull/14546))
- fix: respect the unidirectional nature of time ([#14541](https://github.com/sveltejs/svelte/pull/14541))
## 5.5.3
### Patch Changes
- fix: don't try to add owners to non-`$state` class fields ([#14533](https://github.com/sveltejs/svelte/pull/14533))
- fix: capture infinite_loop_guard in error boundary ([#14534](https://github.com/sveltejs/svelte/pull/14534))
- fix: proxify values when assigning using `||=`, `&&=` and `??=` operators ([#14273](https://github.com/sveltejs/svelte/pull/14273))
## 5.5.2
### Patch Changes
- fix: use correct reaction when lazily creating deriveds inside `SvelteDate` ([#14525](https://github.com/sveltejs/svelte/pull/14525))
## 5.5.1
### Patch Changes
- fix: don't throw with nullish actions ([#13559](https://github.com/sveltejs/svelte/pull/13559))
- fix: leave update expressions untransformed unless a transformer is provided ([#14507](https://github.com/sveltejs/svelte/pull/14507))
- chore: turn reactive_declaration_non_reactive_property into a runtime warning ([#14192](https://github.com/sveltejs/svelte/pull/14192))
## 5.5.0
### Minor Changes
- feat: allow snippets to be exported from module scripts ([#14315](https://github.com/sveltejs/svelte/pull/14315))
### Patch Changes
- fix: ignore TypeScript generics on variables ([#14509](https://github.com/sveltejs/svelte/pull/14509))
## 5.4.0
### Minor Changes
- feat: support `#each` without `as` ([#14396](https://github.com/sveltejs/svelte/pull/14396))
## 5.3.2
### Patch Changes
- fix: correctly prune CSS for elements inside snippets ([#14494](https://github.com/sveltejs/svelte/pull/14494))
- fix: render attributes during SSR regardless of case ([#14492](https://github.com/sveltejs/svelte/pull/14492))
## 5.3.1
### Patch Changes
- fix: treat spread elements the same as call expressions ([#14488](https://github.com/sveltejs/svelte/pull/14488))
- fix: correctly increment/decrement bigints ([#14485](https://github.com/sveltejs/svelte/pull/14485))
## 5.3.0
### Minor Changes
- feat: add error boundaries with `<svelte:boundary>` ([#14211](https://github.com/sveltejs/svelte/pull/14211))
## 5.2.12
### Patch Changes
- fix: upgrade to esm-env 1.2.1 to fix issues with non-Vite setups ([#14470](https://github.com/sveltejs/svelte/pull/14470))
- fix: prevent infinite loops when pruning CSS ([#14474](https://github.com/sveltejs/svelte/pull/14474))
- fix: generate correct code when encountering object expression statement ([#14480](https://github.com/sveltejs/svelte/pull/14480))
## 5.2.11
### Patch Changes
- fix: ignore text and expressions outside the template when validating HTML ([#14468](https://github.com/sveltejs/svelte/pull/14468))
- fix: better account for render tags when pruning CSS ([#14456](https://github.com/sveltejs/svelte/pull/14456))
## 5.2.10
### Patch Changes
- fix: correctly remove unused selectors in middle of selector lists ([#14448](https://github.com/sveltejs/svelte/pull/14448))
- chore: upgrade esm-env for Vite 6 support ([#14460](https://github.com/sveltejs/svelte/pull/14460))
- fix: strip exported TypeScript function overloads ([#14458](https://github.com/sveltejs/svelte/pull/14458))
## 5.2.9 ## 5.2.9
### Patch Changes ### Patch Changes

@ -1103,6 +1103,11 @@ export interface HTMLInputAttributes extends HTMLAttributes<HTMLInputElement> {
step?: number | string | undefined | null; step?: number | string | undefined | null;
type?: HTMLInputTypeAttribute | undefined | null; type?: HTMLInputTypeAttribute | undefined | null;
value?: any; value?: any;
// needs both casing variants because language tools does lowercase names of non-shorthand attributes
defaultValue?: any;
defaultvalue?: any;
defaultChecked?: any;
defaultchecked?: any;
width?: number | string | undefined | null; width?: number | string | undefined | null;
webkitdirectory?: boolean | undefined | null; webkitdirectory?: boolean | undefined | null;
@ -1384,6 +1389,9 @@ export interface HTMLTextareaAttributes extends HTMLAttributes<HTMLTextAreaEleme
required?: boolean | undefined | null; required?: boolean | undefined | null;
rows?: number | undefined | null; rows?: number | undefined | null;
value?: string | string[] | number | undefined | null; value?: string | string[] | number | undefined | null;
// needs both casing variants because language tools does lowercase names of non-shorthand attributes
defaultValue?: string | string[] | number | undefined | null;
defaultvalue?: string | string[] | number | undefined | null;
wrap?: 'hard' | 'soft' | undefined | null; wrap?: 'hard' | 'soft' | undefined | null;
'on:change'?: ChangeEventHandler<HTMLTextAreaElement> | undefined | null; 'on:change'?: ChangeEventHandler<HTMLTextAreaElement> | undefined | null;
@ -2045,6 +2053,10 @@ export interface SvelteHTMLElements {
[name: string]: any; [name: string]: any;
}; };
'svelte:head': { [name: string]: any }; 'svelte:head': { [name: string]: any };
'svelte:boundary': {
onerror?: (error: unknown, reset: () => void) => void;
failed?: import('svelte').Snippet<[error: unknown, reset: () => void]>;
};
[name: string]: { [name: string]: any }; [name: string]: { [name: string]: any };
} }

@ -1,3 +1,35 @@
## assignment_value_stale
> Assignment to `%property%` property (%location%) will evaluate to the right-hand side, not the value of `%property%` following the assignment. This may result in unexpected behaviour.
Given a case like this...
```svelte
<script>
let object = $state({ array: null });
function add() {
(object.array ??= []).push(object.array.length);
}
</script>
<button onclick={add}>add</button>
<p>items: {JSON.stringify(object.items)}</p>
```
...the array being pushed to when the button is first clicked is the `[]` on the right-hand side of the assignment, but the resulting value of `object.array` is an empty state proxy. As a result, the pushed value will be discarded.
You can fix this by separating it into two statements:
```js
let object = { array: [0] };
// ---cut---
function add() {
object.array ??= [];
object.array.push(object.array.length);
}
```
## binding_property_non_reactive ## binding_property_non_reactive
> `%binding%` is binding to a non-reactive property > `%binding%` is binding to a non-reactive property
@ -54,6 +86,44 @@ The easiest way to log a value as it changes over time is to use the [`$inspect`
> %component% mutated a value owned by %owner%. This is strongly discouraged. Consider passing values to child components with `bind:`, or use a callback instead > %component% mutated a value owned by %owner%. This is strongly discouraged. Consider passing values to child components with `bind:`, or use a callback instead
## reactive_declaration_non_reactive_property
> A `$:` statement (%location%) read reactive state that was not visible to the compiler. Updates to this state will not cause the statement to re-run. The behaviour of this code will change if you migrate it to runes mode
In legacy mode, a `$:` [reactive statement](https://svelte.dev/docs/svelte/legacy-reactive-assignments) re-runs when the state it _references_ changes. This is determined at compile time, by analysing the code.
In runes mode, effects and deriveds re-run when there are changes to the values that are read during the function's _execution_.
Often, the result is the same — for example these can be considered equivalent:
```js
let a = 1, b = 2, sum = 3;
// ---cut---
$: sum = a + b;
```
```js
let a = 1, b = 2;
// ---cut---
const sum = $derived(a + b);
```
In some cases — such as the one that triggered the above warning — they are _not_ the same:
```js
let a = 1, b = 2, sum = 3;
// ---cut---
const add = () => a + b;
// the compiler can't 'see' that `sum` depends on `a` and `b`, but
// they _would_ be read while executing the `$derived` version
$: sum = add();
```
Similarly, reactive properties of [deep state](https://svelte.dev/docs/svelte/$state#Deep-state) are not visible to the compiler. As such, changes to these properties will cause effects and deriveds to re-run but will _not_ cause `$:` statements to re-run.
When you [migrate this component](https://svelte.dev/docs/svelte/v5-migration-guide) to runes mode, the behaviour will change accordingly.
## state_proxy_equality_mismatch ## state_proxy_equality_mismatch
> Reactive `$state(...)` proxies and the values they proxy have different identities. Because of this, comparisons with `%operator%` will produce unexpected results > Reactive `$state(...)` proxies and the values they proxy have different identities. Because of this, comparisons with `%operator%` will produce unexpected results

@ -38,6 +38,10 @@
> `$effect()` can only be used as an expression statement > `$effect()` can only be used as an expression statement
## export_undefined
> `%name%` is not defined
## global_reference_invalid ## global_reference_invalid
> `%name%` is an illegal variable name. To reference a global variable called `%name%`, use `globalThis.%name%` > `%name%` is an illegal variable name. To reference a global variable called `%name%`, use `globalThis.%name%`
@ -134,6 +138,28 @@
> %name% cannot be used in runes mode > %name% cannot be used in runes mode
## snippet_invalid_export
> An exported snippet can only reference things declared in a `<script module>`, or other exportable snippets
It's possible to export a snippet from a `<script module>` block, but only if it doesn't reference anything defined inside a non-module-level `<script>`. For example you can't do this...
```svelte
<script module>
export { greeting };
</script>
<script>
let message = 'hello';
</script>
{#snippet greeting(name)}
<p>{message} {name}!</p>
{/snippet}
```
...because `greeting` references `message`, which is defined in the second `<script>`.
## snippet_parameter_assignment ## snippet_parameter_assignment
> Cannot reassign or bind to snippet parameter > Cannot reassign or bind to snippet parameter

@ -190,11 +190,11 @@
## node_invalid_placement ## node_invalid_placement
> %thing% is invalid inside `<%parent%>` > %message%. The browser will 'repair' the HTML (by moving, removing, or inserting elements) which breaks Svelte's assumptions about the structure of your components.
HTML restricts where certain elements can appear. In case of a violation the browser will 'repair' the HTML in a way that breaks Svelte's assumptions about the structure of your components. Some examples: HTML restricts where certain elements can appear. In case of a violation the browser will 'repair' the HTML in a way that breaks Svelte's assumptions about the structure of your components. Some examples:
- `<p>hello <div>world</div></p>` will result in `<p>hello </p><div>world</div><p></p>` for example (the `<div>` autoclosed the `<p>` because `<p>` cannot contain block-level elements) - `<p>hello <div>world</div></p>` will result in `<p>hello </p><div>world</div><p></p>` (the `<div>` autoclosed the `<p>` because `<p>` cannot contain block-level elements)
- `<option><div>option a</div></option>` will result in `<option>option a</option>` (the `<div>` is removed) - `<option><div>option a</div></option>` will result in `<option>option a</option>` (the `<div>` is removed)
- `<table><tr><td>cell</td></tr></table>` will result in `<table><tbody><tr><td>cell</td></tr></tbody></table>` (a `<tbody>` is auto-inserted) - `<table><tr><td>cell</td></tr></table>` will result in `<table><tbody><tr><td>cell</td></tr></tbody></table>` (a `<tbody>` is auto-inserted)
@ -282,6 +282,14 @@ HTML restricts where certain elements can appear. In case of a violation the bro
> `<svelte:body>` does not support non-event attributes or spread attributes > `<svelte:body>` does not support non-event attributes or spread attributes
## svelte_boundary_invalid_attribute
> Valid attributes on `<svelte:boundary>` are `onerror` and `failed`
## svelte_boundary_invalid_attribute_value
> Attribute value must be a non-string expression
## svelte_component_invalid_this ## svelte_component_invalid_this
> Invalid component definition — must be an `{expression}` > Invalid component definition — must be an `{expression}`

@ -26,10 +26,6 @@
> Reassignments of module-level declarations will not cause reactive statements to update > Reassignments of module-level declarations will not cause reactive statements to update
## reactive_declaration_non_reactive_property
> Properties of objects and arrays are not reactive unless in runes mode. Changes to this property will not cause the reactive statement to update
## state_referenced_locally ## state_referenced_locally
> State referenced in its own scope will never update. Did you mean to reference it inside a closure? > State referenced in its own scope will never update. Did you mean to reference it inside a closure?

@ -40,11 +40,11 @@
## node_invalid_placement_ssr ## node_invalid_placement_ssr
> %thing% is invalid inside `<%parent%>`. When rendering this component on the server, the resulting HTML will be modified by the browser, likely resulting in a `hydration_mismatch` warning > %message%. When rendering this component on the server, the resulting HTML will be modified by the browser (by moving, removing, or inserting elements), likely resulting in a `hydration_mismatch` warning
HTML restricts where certain elements can appear. In case of a violation the browser will 'repair' the HTML in a way that breaks Svelte's assumptions about the structure of your components. Some examples: HTML restricts where certain elements can appear. In case of a violation the browser will 'repair' the HTML in a way that breaks Svelte's assumptions about the structure of your components. Some examples:
- `<p>hello <div>world</div></p>` will result in `<p>hello </p><div>world</div><p></p>` for example (the `<div>` autoclosed the `<p>` because `<p>` cannot contain block-level elements) - `<p>hello <div>world</div></p>` will result in `<p>hello </p><div>world</div><p></p>` (the `<div>` autoclosed the `<p>` because `<p>` cannot contain block-level elements)
- `<option><div>option a</div></option>` will result in `<option>option a</option>` (the `<div>` is removed) - `<option><div>option a</div></option>` will result in `<option>option a</option>` (the `<div>` is removed)
- `<table><tr><td>cell</td></tr></table>` will result in `<table><tbody><tr><td>cell</td></tr></tbody></table>` (a `<tbody>` is auto-inserted) - `<table><tr><td>cell</td></tr></table>` will result in `<table><tbody><tr><td>cell</td></tr></tbody></table>` (a `<tbody>` is auto-inserted)

@ -2,7 +2,7 @@
"name": "svelte", "name": "svelte",
"description": "Cybernetically enhanced web apps", "description": "Cybernetically enhanced web apps",
"license": "MIT", "license": "MIT",
"version": "5.2.9", "version": "5.7.1",
"type": "module", "type": "module",
"types": "./types/index.d.ts", "types": "./types/index.d.ts",
"engines": { "engines": {
@ -142,8 +142,8 @@
"acorn-typescript": "^1.4.13", "acorn-typescript": "^1.4.13",
"aria-query": "^5.3.1", "aria-query": "^5.3.1",
"axobject-query": "^4.1.0", "axobject-query": "^4.1.0",
"esm-env": "^1.0.0", "esm-env": "^1.2.1",
"esrap": "^1.2.2", "esrap": "^1.2.3",
"is-reference": "^3.0.3", "is-reference": "^3.0.3",
"locate-character": "^3.0.0", "locate-character": "^3.0.0",
"magic-string": "^0.30.11", "magic-string": "^0.30.11",

@ -168,6 +168,16 @@ export function effect_invalid_placement(node) {
e(node, "effect_invalid_placement", "`$effect()` can only be used as an expression statement"); e(node, "effect_invalid_placement", "`$effect()` can only be used as an expression statement");
} }
/**
* `%name%` is not defined
* @param {null | number | NodeLike} node
* @param {string} name
* @returns {never}
*/
export function export_undefined(node, name) {
e(node, "export_undefined", `\`${name}\` is not defined`);
}
/** /**
* `%name%` is an illegal variable name. To reference a global variable called `%name%`, use `globalThis.%name%` * `%name%` is an illegal variable name. To reference a global variable called `%name%`, use `globalThis.%name%`
* @param {null | number | NodeLike} node * @param {null | number | NodeLike} node
@ -395,6 +405,15 @@ export function runes_mode_invalid_import(node, name) {
e(node, "runes_mode_invalid_import", `${name} cannot be used in runes mode`); e(node, "runes_mode_invalid_import", `${name} cannot be used in runes mode`);
} }
/**
* An exported snippet can only reference things declared in a `<script module>`, or other exportable snippets
* @param {null | number | NodeLike} node
* @returns {never}
*/
export function snippet_invalid_export(node) {
e(node, "snippet_invalid_export", "An exported snippet can only reference things declared in a `<script module>`, or other exportable snippets");
}
/** /**
* Cannot reassign or bind to snippet parameter * Cannot reassign or bind to snippet parameter
* @param {null | number | NodeLike} node * @param {null | number | NodeLike} node
@ -1024,14 +1043,13 @@ export function mixed_event_handler_syntaxes(node, name) {
} }
/** /**
* %thing% is invalid inside `<%parent%>` * %message%. The browser will 'repair' the HTML (by moving, removing, or inserting elements) which breaks Svelte's assumptions about the structure of your components.
* @param {null | number | NodeLike} node * @param {null | number | NodeLike} node
* @param {string} thing * @param {string} message
* @param {string} parent
* @returns {never} * @returns {never}
*/ */
export function node_invalid_placement(node, thing, parent) { export function node_invalid_placement(node, message) {
e(node, "node_invalid_placement", `${thing} is invalid inside \`<${parent}>\``); e(node, "node_invalid_placement", `${message}. The browser will 'repair' the HTML (by moving, removing, or inserting elements) which breaks Svelte's assumptions about the structure of your components.`);
} }
/** /**
@ -1228,6 +1246,24 @@ export function svelte_body_illegal_attribute(node) {
e(node, "svelte_body_illegal_attribute", "`<svelte:body>` does not support non-event attributes or spread attributes"); e(node, "svelte_body_illegal_attribute", "`<svelte:body>` does not support non-event attributes or spread attributes");
} }
/**
* Valid attributes on `<svelte:boundary>` are `onerror` and `failed`
* @param {null | number | NodeLike} node
* @returns {never}
*/
export function svelte_boundary_invalid_attribute(node) {
e(node, "svelte_boundary_invalid_attribute", "Valid attributes on `<svelte:boundary>` are `onerror` and `failed`");
}
/**
* Attribute value must be a non-string expression
* @param {null | number | NodeLike} node
* @returns {never}
*/
export function svelte_boundary_invalid_attribute_value(node) {
e(node, "svelte_boundary_invalid_attribute_value", "Attribute value must be a non-string expression");
}
/** /**
* Invalid component definition must be an `{expression}` * Invalid component definition must be an `{expression}`
* @param {null | number | NodeLike} node * @param {null | number | NodeLike} node

@ -381,6 +381,20 @@ export function convert(source, ast) {
children: node.body.nodes.map((child) => visit(child)) children: node.body.nodes.map((child) => visit(child))
}; };
}, },
// @ts-expect-error
SvelteBoundary(node, { visit }) {
remove_surrounding_whitespace_nodes(node.fragment.nodes);
return {
type: 'SvelteBoundary',
name: 'svelte:boundary',
start: node.start,
end: node.end,
attributes: node.attributes.map(
(child) => /** @type {Legacy.LegacyAttributeLike} */ (visit(child))
),
children: node.fragment.nodes.map((child) => visit(child))
};
},
RegularElement(node, { visit }) { RegularElement(node, { visit }) {
return { return {
type: 'Element', type: 'Element',

@ -10,17 +10,43 @@ const ParserWithTS = acorn.Parser.extend(tsPlugin({ allowSatisfies: true }));
/** /**
* @param {string} source * @param {string} source
* @param {boolean} typescript * @param {boolean} typescript
* @param {boolean} [is_script]
*/ */
export function parse(source, typescript) { export function parse(source, typescript, is_script) {
const parser = typescript ? ParserWithTS : acorn.Parser; const parser = typescript ? ParserWithTS : acorn.Parser;
const { onComment, add_comments } = get_comment_handlers(source); const { onComment, add_comments } = get_comment_handlers(source);
// @ts-ignore
const parse_statement = parser.prototype.parseStatement;
// If we're dealing with a <script> then it might contain an export
// for something that doesn't exist directly inside but is inside the
// component instead, so we need to ensure that Acorn doesn't throw
// an error in these cases
if (is_script) {
// @ts-ignore
parser.prototype.parseStatement = function (...args) {
const v = parse_statement.call(this, ...args);
// @ts-ignore
this.undefinedExports = {};
return v;
};
}
let ast;
const ast = parser.parse(source, { try {
ast = parser.parse(source, {
onComment, onComment,
sourceType: 'module', sourceType: 'module',
ecmaVersion: 13, ecmaVersion: 13,
locations: true locations: true
}); });
} finally {
if (is_script) {
// @ts-ignore
parser.prototype.parseStatement = parse_statement;
}
}
if (typescript) amend(source, ast); if (typescript) amend(source, ast);
add_comments(ast); add_comments(ast);

@ -34,7 +34,7 @@ export function read_script(parser, start, attributes) {
let ast; let ast;
try { try {
ast = acorn.parse(source, parser.ts); ast = acorn.parse(source, parser.ts, true);
} catch (err) { } catch (err) {
parser.acorn_error(err); parser.acorn_error(err);
} }

@ -36,7 +36,11 @@ const visitors = {
if (node.exportKind === 'type') return b.empty; if (node.exportKind === 'type') return b.empty;
if (node.declaration) { if (node.declaration) {
return context.next(); const result = context.next();
if (result?.declaration?.type === 'EmptyStatement') {
return b.empty;
}
return result;
} }
if (node.specifiers) { if (node.specifiers) {
@ -100,6 +104,9 @@ const visitors = {
} }
return context.visit(node.parameter); return context.visit(node.parameter);
}, },
TSInstantiationExpression(node, context) {
return context.visit(node.expression);
},
FunctionExpression: remove_this_param, FunctionExpression: remove_this_param,
FunctionDeclaration: remove_this_param, FunctionDeclaration: remove_this_param,
TSDeclareFunction() { TSDeclareFunction() {

@ -44,7 +44,8 @@ const meta_tags = new Map([
['svelte:element', 'SvelteElement'], ['svelte:element', 'SvelteElement'],
['svelte:component', 'SvelteComponent'], ['svelte:component', 'SvelteComponent'],
['svelte:self', 'SvelteSelf'], ['svelte:self', 'SvelteSelf'],
['svelte:fragment', 'SvelteFragment'] ['svelte:fragment', 'SvelteFragment'],
['svelte:boundary', 'SvelteBoundary']
]); ]);
/** @param {Parser} parser */ /** @param {Parser} parser */

@ -1,4 +1,4 @@
/** @import { ArrowFunctionExpression, Expression, Identifier } from 'estree' */ /** @import { ArrowFunctionExpression, Expression, Identifier, Pattern } from 'estree' */
/** @import { AST } from '#compiler' */ /** @import { AST } from '#compiler' */
/** @import { Parser } from '../index.js' */ /** @import { Parser } from '../index.js' */
import read_pattern from '../read/context.js'; import read_pattern from '../read/context.js';
@ -142,16 +142,25 @@ function open(parser) {
parser.index = end; parser.index = end;
} }
} }
parser.eat('as', true);
/** @type {Pattern | null} */
let context = null;
let index;
let key;
if (parser.eat('as')) {
parser.require_whitespace(); parser.require_whitespace();
const context = read_pattern(parser); context = read_pattern(parser);
} else {
// {#each Array.from({ length: 10 }), i} is read as a sequence expression,
// which is set back above - we now gotta reset the index as a consequence
// to properly read the , i part
parser.index = /** @type {number} */ (expression.end);
}
parser.allow_whitespace(); parser.allow_whitespace();
let index;
let key;
if (parser.eat(',')) { if (parser.eat(',')) {
parser.allow_whitespace(); parser.allow_whitespace();
index = parser.read_identifier(); index = parser.read_identifier();
@ -314,7 +323,11 @@ function open(parser) {
name name
}, },
parameters: function_expression.params, parameters: function_expression.params,
body: create_fragment() body: create_fragment(),
metadata: {
can_hoist: false,
sites: new Set()
}
}); });
parser.stack.push(block); parser.stack.push(block);
parser.fragments.push(block.body); parser.fragments.push(block.body);
@ -605,7 +618,8 @@ function special(parser) {
metadata: { metadata: {
dynamic: false, dynamic: false,
args_with_call_expression: new Set(), args_with_call_expression: new Set(),
path: [] path: [],
snippets: new Set()
} }
}); });
} }

@ -1,16 +1,9 @@
/** @import { Visitors } from 'zimmerframe' */
/** @import * as Compiler from '#compiler' */ /** @import * as Compiler from '#compiler' */
import { walk } from 'zimmerframe'; import { walk } from 'zimmerframe';
import { get_parent_rules, get_possible_values, is_outer_global } from './utils.js'; import { get_parent_rules, get_possible_values, is_outer_global } from './utils.js';
import { regex_ends_with_whitespace, regex_starts_with_whitespace } from '../../patterns.js'; import { regex_ends_with_whitespace, regex_starts_with_whitespace } from '../../patterns.js';
import { get_attribute_chunks, is_text_attribute } from '../../../utils/ast.js'; import { get_attribute_chunks, is_text_attribute } from '../../../utils/ast.js';
/**
* @typedef {{
* element: Compiler.AST.RegularElement | Compiler.AST.SvelteElement;
* from_render_tag: boolean;
* }} State
*/
/** @typedef {NODE_PROBABLY_EXISTS | NODE_DEFINITELY_EXISTS} NodeExistsValue */ /** @typedef {NODE_PROBABLY_EXISTS | NODE_DEFINITELY_EXISTS} NodeExistsValue */
const NODE_PROBABLY_EXISTS = 0; const NODE_PROBABLY_EXISTS = 0;
@ -50,24 +43,19 @@ const nesting_selector = {
} }
}; };
/**
* Snippets encountered already (avoids infinite loops)
* @type {Set<Compiler.AST.SnippetBlock>}
*/
const seen = new Set();
/** /**
* *
* @param {Compiler.Css.StyleSheet} stylesheet * @param {Compiler.Css.StyleSheet} stylesheet
* @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement | Compiler.AST.RenderTag} element * @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement} element
*/ */
export function prune(stylesheet, element) { export function prune(stylesheet, element) {
if (element.type === 'RenderTag') { walk(/** @type {Compiler.Css.Node} */ (stylesheet), null, {
const parent = get_element_parent(element);
if (!parent) return;
walk(stylesheet, { element: parent, from_render_tag: true }, visitors);
} else {
walk(stylesheet, { element, from_render_tag: false }, visitors);
}
}
/** @type {Visitors<Compiler.Css.Node, State>} */
const visitors = {
Rule(node, context) { Rule(node, context) {
if (node.metadata.is_global_block) { if (node.metadata.is_global_block) {
context.visit(node.prelude); context.visit(node.prelude);
@ -75,48 +63,14 @@ const visitors = {
context.next(); context.next();
} }
}, },
ComplexSelector(node, context) { ComplexSelector(node) {
const selectors = get_relative_selectors(node); const selectors = get_relative_selectors(node);
const inner = selectors[selectors.length - 1];
if (context.state.from_render_tag) {
// We're searching for a match that crosses a render tag boundary. That means we have to both traverse up
// the element tree (to see if we find an entry point) but also remove selectors from the end (assuming
// they are part of the render tag we don't see). We do all possible combinations of both until we find a match.
/** @type {Compiler.AST.RegularElement | Compiler.AST.SvelteElement | null} */
let element = context.state.element;
while (element) {
const selectors_to_check = selectors.slice();
while (selectors_to_check.length > 0) { seen.clear();
selectors_to_check.pop();
if ( if (
apply_selector( apply_selector(selectors, /** @type {Compiler.Css.Rule} */ (node.metadata.rule), element)
selectors_to_check,
/** @type {Compiler.Css.Rule} */ (node.metadata.rule),
element,
context.state
)
) { ) {
mark(inner, element);
node.metadata.used = true;
return;
}
}
element = get_element_parent(element);
}
} else if (
apply_selector(
selectors,
/** @type {Compiler.Css.Rule} */ (node.metadata.rule),
context.state.element,
context.state
)
) {
mark(inner, context.state.element);
node.metadata.used = true; node.metadata.used = true;
} }
@ -124,7 +78,8 @@ const visitors = {
// selectors that don't belong to rules (i.e. inside `:is(...)` etc) // selectors that don't belong to rules (i.e. inside `:is(...)` etc)
// when we encounter them below // when we encounter them below
} }
}; });
}
/** /**
* Retrieves the relative selectors (minus the trailing globals) from a complex selector. * Retrieves the relative selectors (minus the trailing globals) from a complex selector.
@ -141,16 +96,13 @@ function get_relative_selectors(node) {
// nesting could be inside pseudo classes like :is, :has or :where // nesting could be inside pseudo classes like :is, :has or :where
for (let selector of selectors) { for (let selector of selectors) {
walk( walk(selector, null, {
selector,
{},
{
// @ts-ignore // @ts-ignore
NestingSelector() { NestingSelector() {
has_explicit_nesting_selector = true; has_explicit_nesting_selector = true;
} }
} });
);
// if we found one we can break from the others // if we found one we can break from the others
if (has_explicit_nesting_selector) break; if (has_explicit_nesting_selector) break;
} }
@ -203,89 +155,69 @@ function truncate(node) {
* @param {Compiler.Css.RelativeSelector[]} relative_selectors * @param {Compiler.Css.RelativeSelector[]} relative_selectors
* @param {Compiler.Css.Rule} rule * @param {Compiler.Css.Rule} rule
* @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement} element * @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement} element
* @param {State} state
* @returns {boolean} * @returns {boolean}
*/ */
function apply_selector(relative_selectors, rule, element, state) { function apply_selector(relative_selectors, rule, element) {
const parent_selectors = relative_selectors.slice(); const parent_selectors = relative_selectors.slice();
const relative_selector = parent_selectors.pop(); const relative_selector = parent_selectors.pop();
if (!relative_selector) return false; const matched =
!!relative_selector &&
relative_selector_might_apply_to_node(relative_selector, rule, element) &&
apply_combinator(relative_selector, parent_selectors, rule, element);
const possible_match = relative_selector_might_apply_to_node( if (matched) {
relative_selector, if (!is_outer_global(relative_selector)) {
rule, relative_selector.metadata.scoped = true;
element,
state
);
if (!possible_match) {
return false;
}
if (relative_selector.combinator) {
return apply_combinator(
relative_selector.combinator,
relative_selector,
parent_selectors,
rule,
element,
state
);
} }
// if this is the left-most non-global selector, mark it — we want element.metadata.scoped = true;
// `x y z {...}` to become `x.blah y z.blah {...}`
const parent = parent_selectors[parent_selectors.length - 1];
if (!parent || is_global(parent, rule)) {
mark(relative_selector, element);
} }
return true; return matched;
} }
/** /**
* @param {Compiler.Css.Combinator} combinator
* @param {Compiler.Css.RelativeSelector} relative_selector * @param {Compiler.Css.RelativeSelector} relative_selector
* @param {Compiler.Css.RelativeSelector[]} parent_selectors * @param {Compiler.Css.RelativeSelector[]} parent_selectors
* @param {Compiler.Css.Rule} rule * @param {Compiler.Css.Rule} rule
* @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement} element * @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement | Compiler.AST.RenderTag | Compiler.AST.Component | Compiler.AST.SvelteComponent | Compiler.AST.SvelteSelf} node
* @param {State} state
* @returns {boolean} * @returns {boolean}
*/ */
function apply_combinator(combinator, relative_selector, parent_selectors, rule, element, state) { function apply_combinator(relative_selector, parent_selectors, rule, node) {
const name = combinator.name; if (!relative_selector.combinator) return true;
const name = relative_selector.combinator.name;
switch (name) { switch (name) {
case ' ': case ' ':
case '>': { case '>': {
let parent_matched = false; let parent_matched = false;
let crossed_component_boundary = false;
const path = element.metadata.path; const path = node.metadata.path;
let i = path.length; let i = path.length;
while (i--) { while (i--) {
const parent = path[i]; const parent = path[i];
if (parent.type === 'Component' || parent.type === 'SvelteComponent') {
crossed_component_boundary = true;
}
if (parent.type === 'SnippetBlock') { if (parent.type === 'SnippetBlock') {
// We assume the snippet might be rendered in a place where the parent selectors match. if (seen.has(parent)) {
// (We could do more static analysis and check the render tag reference to see if this snippet block continues parent_matched = true;
// with elements that actually match the selector, but that would be a lot of work for little gain) } else {
return true; seen.add(parent);
for (const site of parent.metadata.sites) {
if (apply_combinator(relative_selector, parent_selectors, rule, site)) {
parent_matched = true;
}
}
} }
if (parent.type === 'RegularElement' || parent.type === 'SvelteElement') { break;
if (apply_selector(parent_selectors, rule, parent, state)) {
// TODO the `name === ' '` causes false positives, but removing it causes false negatives...
if (name === ' ' || crossed_component_boundary) {
mark(parent_selectors[parent_selectors.length - 1], parent);
} }
if (parent.type === 'RegularElement' || parent.type === 'SvelteElement') {
if (apply_selector(parent_selectors, rule, parent)) {
parent_matched = true; parent_matched = true;
} }
@ -298,7 +230,7 @@ function apply_combinator(combinator, relative_selector, parent_selectors, rule,
case '+': case '+':
case '~': { case '~': {
const siblings = get_possible_element_siblings(element, name === '+'); const siblings = get_possible_element_siblings(node, name === '+');
let sibling_matched = false; let sibling_matched = false;
@ -306,18 +238,16 @@ function apply_combinator(combinator, relative_selector, parent_selectors, rule,
if (possible_sibling.type === 'RenderTag' || possible_sibling.type === 'SlotElement') { if (possible_sibling.type === 'RenderTag' || possible_sibling.type === 'SlotElement') {
// `{@render foo()}<p>foo</p>` with `:global(.x) + p` is a match // `{@render foo()}<p>foo</p>` with `:global(.x) + p` is a match
if (parent_selectors.length === 1 && parent_selectors[0].metadata.is_global) { if (parent_selectors.length === 1 && parent_selectors[0].metadata.is_global) {
mark(relative_selector, element);
sibling_matched = true; sibling_matched = true;
} }
} else if (apply_selector(parent_selectors, rule, possible_sibling, state)) { } else if (apply_selector(parent_selectors, rule, possible_sibling)) {
mark(relative_selector, element);
sibling_matched = true; sibling_matched = true;
} }
} }
return ( return (
sibling_matched || sibling_matched ||
(get_element_parent(element) === null && (get_element_parent(node) === null &&
parent_selectors.every((selector) => is_global(selector, rule))) parent_selectors.every((selector) => is_global(selector, rule)))
); );
} }
@ -328,19 +258,6 @@ function apply_combinator(combinator, relative_selector, parent_selectors, rule,
} }
} }
/**
* Mark both the compound selector and the node it selects as encapsulated,
* for transformation in a later step
* @param {Compiler.Css.RelativeSelector} relative_selector
* @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement} element
*/
function mark(relative_selector, element) {
if (!is_outer_global(relative_selector)) {
relative_selector.metadata.scoped = true;
}
element.metadata.scoped = true;
}
/** /**
* Returns `true` if the relative selector is global, meaning * Returns `true` if the relative selector is global, meaning
* it's a `:global(...)` or unscopeable selector, or * it's a `:global(...)` or unscopeable selector, or
@ -392,10 +309,9 @@ const regex_backslash_and_following_character = /\\(.)/g;
* @param {Compiler.Css.RelativeSelector} relative_selector * @param {Compiler.Css.RelativeSelector} relative_selector
* @param {Compiler.Css.Rule} rule * @param {Compiler.Css.Rule} rule
* @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement} element * @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement} element
* @param {State} state * @returns {boolean}
* @returns {boolean }
*/ */
function relative_selector_might_apply_to_node(relative_selector, rule, element, state) { function relative_selector_might_apply_to_node(relative_selector, rule, element) {
// Sort :has(...) selectors in one bucket and everything else into another // Sort :has(...) selectors in one bucket and everything else into another
const has_selectors = []; const has_selectors = [];
const other_selectors = []; const other_selectors = [];
@ -420,7 +336,7 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element,
// If this is a :has inside a global selector, we gotta include the element itself, too, // If this is a :has inside a global selector, we gotta include the element itself, too,
// because the global selector might be for an element that's outside the component (e.g. :root). // because the global selector might be for an element that's outside the component (e.g. :root).
const rules = [rule, ...get_parent_rules(rule)]; const rules = get_parent_rules(rule);
const include_self = const include_self =
rules.some((r) => r.prelude.children.some((c) => c.children.some((s) => is_global(s, r)))) || rules.some((r) => r.prelude.children.some((c) => c.children.some((s) => is_global(s, r)))) ||
rules[rules.length - 1].prelude.children.some((c) => rules[rules.length - 1].prelude.children.some((c) =>
@ -433,10 +349,14 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element,
descendant_elements.push(element); descendant_elements.push(element);
} }
walk( const seen = new Set();
/** @type {Compiler.SvelteNode} */ (element.fragment),
{ is_child: true }, /**
{ * @param {Compiler.SvelteNode} node
* @param {{ is_child: boolean }} state
*/
function walk_children(node, state) {
walk(node, state, {
_(node, context) { _(node, context) {
if (node.type === 'RegularElement' || node.type === 'SvelteElement') { if (node.type === 'RegularElement' || node.type === 'SvelteElement') {
descendant_elements.push(node); descendant_elements.push(node);
@ -449,12 +369,21 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element,
} else { } else {
context.next(); 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 { } else {
context.next(); 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 // :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 // upwards and back-to-front, we need to first check the selectors inside :has(...), then check the rest of the
@ -490,7 +419,7 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element,
if ( if (
selectors.length === 0 /* is :global(...) */ || selectors.length === 0 /* is :global(...) */ ||
(element.metadata.scoped && selector_matched) || (element.metadata.scoped && selector_matched) ||
apply_selector(selectors, rule, element, state) apply_selector(selectors, rule, element)
) { ) {
complex_selector.metadata.used = true; complex_selector.metadata.used = true;
selector_matched = matched = true; selector_matched = matched = true;
@ -520,7 +449,7 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element,
) { ) {
const args = selector.args; const args = selector.args;
const complex_selector = args.children[0]; const complex_selector = args.children[0];
return apply_selector(complex_selector.children, rule, element, state); return apply_selector(complex_selector.children, rule, element);
} }
// We came across a :global, everything beyond it is global and therefore a potential match // We came across a :global, everything beyond it is global and therefore a potential match
@ -569,7 +498,7 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element,
if (is_global) { if (is_global) {
complex_selector.metadata.used = true; complex_selector.metadata.used = true;
matched = true; matched = true;
} else if (apply_selector(relative, rule, element, state)) { } else if (apply_selector(relative, rule, element)) {
complex_selector.metadata.used = true; complex_selector.metadata.used = true;
matched = true; matched = true;
} else if (complex_selector.children.length > 1 && (name == 'is' || name == 'where')) { } else if (complex_selector.children.length > 1 && (name == 'is' || name == 'where')) {
@ -653,7 +582,7 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element,
for (const complex_selector of parent.prelude.children) { for (const complex_selector of parent.prelude.children) {
if ( if (
apply_selector(get_relative_selectors(complex_selector), parent, element, state) || apply_selector(get_relative_selectors(complex_selector), parent, element) ||
complex_selector.children.every((s) => is_global(s, parent)) complex_selector.children.every((s) => is_global(s, parent))
) { ) {
complex_selector.metadata.used = true; complex_selector.metadata.used = true;
@ -707,17 +636,33 @@ function get_following_sibling_elements(element, include_self) {
// ...then walk them, starting from the node after the one // ...then walk them, starting from the node after the one
// containing the element in question // containing the element in question
for (const node of nodes.slice(nodes.indexOf(start) + 1)) {
const seen = new Set();
/** @param {Compiler.SvelteNode} node */
function get_siblings(node) {
walk(node, null, { walk(node, null, {
RegularElement(node) { RegularElement(node) {
siblings.push(node); siblings.push(node);
}, },
SvelteElement(node) { SvelteElement(node) {
siblings.push(node); siblings.push(node);
},
RenderTag(node) {
for (const snippet of node.metadata.snippets) {
if (seen.has(snippet)) continue;
seen.add(snippet);
get_siblings(snippet.body);
}
} }
}); });
} }
for (const node of nodes.slice(nodes.indexOf(start) + 1)) {
get_siblings(node);
}
if (include_self) { if (include_self) {
siblings.push(element); siblings.push(element);
} }
@ -862,7 +807,7 @@ function unquote(str) {
} }
/** /**
* @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement | Compiler.AST.RenderTag} node * @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} * @returns {Compiler.AST.RegularElement | Compiler.AST.SvelteElement | null}
*/ */
function get_element_parent(node) { function get_element_parent(node) {
@ -881,17 +826,18 @@ function get_element_parent(node) {
} }
/** /**
* @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement} element * @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 {boolean} adjacent_only
* @param {Set<Compiler.AST.SnippetBlock>} seen
* @returns {Map<Compiler.AST.RegularElement | Compiler.AST.SvelteElement | Compiler.AST.SlotElement | Compiler.AST.RenderTag, NodeExistsValue>} * @returns {Map<Compiler.AST.RegularElement | Compiler.AST.SvelteElement | Compiler.AST.SlotElement | Compiler.AST.RenderTag, NodeExistsValue>}
*/ */
function get_possible_element_siblings(element, adjacent_only) { function get_possible_element_siblings(node, adjacent_only, seen = new Set()) {
/** @type {Map<Compiler.AST.RegularElement | Compiler.AST.SvelteElement | Compiler.AST.SlotElement | Compiler.AST.RenderTag, NodeExistsValue>} */ /** @type {Map<Compiler.AST.RegularElement | Compiler.AST.SvelteElement | Compiler.AST.SlotElement | Compiler.AST.RenderTag, NodeExistsValue>} */
const result = new Map(); const result = new Map();
const path = element.metadata.path; const path = node.metadata.path;
/** @type {Compiler.SvelteNode} */ /** @type {Compiler.SvelteNode} */
let current = element; let current = node;
let i = path.length; let i = path.length;
@ -933,7 +879,31 @@ function get_possible_element_siblings(element, adjacent_only) {
current = path[i]; current = path[i];
if (!current || !is_block(current)) break; if (!current) break;
if (
current.type === 'Component' ||
current.type === 'SvelteComponent' ||
current.type === 'SvelteSelf'
) {
continue;
}
if (current.type === 'SnippetBlock') {
if (seen.has(current)) break;
seen.add(current);
for (const site of current.metadata.sites) {
const siblings = get_possible_element_siblings(site, adjacent_only, seen);
add_to_map(siblings, result);
if (adjacent_only && current.metadata.sites.size === 1 && has_definite_elements(siblings)) {
return result;
}
}
}
if (!is_block(current)) break;
if (current.type === 'EachBlock' && fragment === current.body) { if (current.type === 'EachBlock' && fragment === current.body) {
// `{#each ...}<a /><b />{/each}` — `<b>` can be previous sibling of `<a />` // `{#each ...}<a /><b />{/each}` — `<b>` can be previous sibling of `<a />`

@ -39,15 +39,14 @@ export function get_possible_values(chunk) {
* @param {Css.Rule | null} rule * @param {Css.Rule | null} rule
*/ */
export function get_parent_rules(rule) { export function get_parent_rules(rule) {
const parents = []; const rules = [];
let parent = rule?.metadata.parent_rule; while (rule) {
while (parent) { rules.push(rule);
parents.push(parent); rule = rule.metadata.parent_rule;
parent = parent.metadata.parent_rule;
} }
return parents; return rules;
} }
/** /**

@ -51,6 +51,7 @@ import { RenderTag } from './visitors/RenderTag.js';
import { SlotElement } from './visitors/SlotElement.js'; import { SlotElement } from './visitors/SlotElement.js';
import { SnippetBlock } from './visitors/SnippetBlock.js'; import { SnippetBlock } from './visitors/SnippetBlock.js';
import { SpreadAttribute } from './visitors/SpreadAttribute.js'; import { SpreadAttribute } from './visitors/SpreadAttribute.js';
import { SpreadElement } from './visitors/SpreadElement.js';
import { StyleDirective } from './visitors/StyleDirective.js'; import { StyleDirective } from './visitors/StyleDirective.js';
import { SvelteBody } from './visitors/SvelteBody.js'; import { SvelteBody } from './visitors/SvelteBody.js';
import { SvelteComponent } from './visitors/SvelteComponent.js'; import { SvelteComponent } from './visitors/SvelteComponent.js';
@ -61,6 +62,7 @@ import { SvelteHead } from './visitors/SvelteHead.js';
import { SvelteHTML } from './visitors/SvelteHTML.js'; import { SvelteHTML } from './visitors/SvelteHTML.js';
import { SvelteSelf } from './visitors/SvelteSelf.js'; import { SvelteSelf } from './visitors/SvelteSelf.js';
import { SvelteWindow } from './visitors/SvelteWindow.js'; import { SvelteWindow } from './visitors/SvelteWindow.js';
import { SvelteBoundary } from './visitors/SvelteBoundary.js';
import { TaggedTemplateExpression } from './visitors/TaggedTemplateExpression.js'; import { TaggedTemplateExpression } from './visitors/TaggedTemplateExpression.js';
import { Text } from './visitors/Text.js'; import { Text } from './visitors/Text.js';
import { TitleElement } from './visitors/TitleElement.js'; import { TitleElement } from './visitors/TitleElement.js';
@ -163,6 +165,7 @@ const visitors = {
SlotElement, SlotElement,
SnippetBlock, SnippetBlock,
SpreadAttribute, SpreadAttribute,
SpreadElement,
StyleDirective, StyleDirective,
SvelteBody, SvelteBody,
SvelteComponent, SvelteComponent,
@ -173,6 +176,7 @@ const visitors = {
SvelteHTML, SvelteHTML,
SvelteSelf, SvelteSelf,
SvelteWindow, SvelteWindow,
SvelteBoundary,
TaggedTemplateExpression, TaggedTemplateExpression,
Text, Text,
TransitionDirective, TransitionDirective,
@ -427,7 +431,6 @@ export function analyze_component(root, source, options) {
reactive_statements: new Map(), reactive_statements: new Map(),
binding_groups: new Map(), binding_groups: new Map(),
slot_names: new Map(), slot_names: new Map(),
top_level_snippets: [],
css: { css: {
ast: root.css, ast: root.css,
hash: root.css hash: root.css
@ -440,7 +443,10 @@ export function analyze_component(root, source, options) {
: '', : '',
keyframes: [] keyframes: []
}, },
source source,
undefined_exports: new Map(),
snippet_renderers: new Map(),
snippets: new Set()
}; };
if (!runes) { if (!runes) {
@ -693,6 +699,17 @@ export function analyze_component(root, source, options) {
analysis.reactive_statements = order_reactive_statements(analysis.reactive_statements); analysis.reactive_statements = order_reactive_statements(analysis.reactive_statements);
} }
for (const node of analysis.module.ast.body) {
if (node.type === 'ExportNamedDeclaration' && node.specifiers !== null) {
for (const specifier of node.specifiers) {
if (specifier.local.type !== 'Identifier') continue;
const binding = analysis.module.scope.get(specifier.local.name);
if (!binding) e.export_undefined(specifier, specifier.local.name);
}
}
}
if (analysis.event_directive_node && analysis.uses_event_attributes) { if (analysis.event_directive_node && analysis.uses_event_attributes) {
e.mixed_event_handler_syntaxes( e.mixed_event_handler_syntaxes(
analysis.event_directive_node, analysis.event_directive_node,
@ -700,6 +717,16 @@ export function analyze_component(root, source, options) {
); );
} }
for (const [node, resolved] of analysis.snippet_renderers) {
if (!resolved) {
node.metadata.snippets = analysis.snippets;
}
for (const snippet of node.metadata.snippets) {
snippet.metadata.sites.add(node);
}
}
if ( if (
analysis.uses_render_tags && analysis.uses_render_tags &&
(analysis.uses_slots || (!analysis.custom_element && analysis.slot_names.size > 0)) (analysis.uses_slots || (!analysis.custom_element && analysis.slot_names.size > 0))
@ -728,8 +755,6 @@ export function analyze_component(root, source, options) {
} }
outer: for (const node of analysis.elements) { outer: for (const node of analysis.elements) {
if (node.type === 'RenderTag') continue;
if (node.metadata.scoped) { 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 // 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 // TODO this happens during the analysis phase, which shouldn't know anything about client vs server

@ -16,7 +16,7 @@ export function EachBlock(node, context) {
validate_block_not_empty(node.fallback, context); validate_block_not_empty(node.fallback, context);
const id = node.context; const id = node.context;
if (id.type === 'Identifier' && (id.name === '$state' || id.name === '$derived')) { if (id?.type === 'Identifier' && (id.name === '$state' || id.name === '$derived')) {
// TODO weird that this is necessary // TODO weird that this is necessary
e.state_invalid_placement(node, id.name); e.state_invalid_placement(node, id.name);
} }

@ -9,11 +9,12 @@ import { mark_subtree_dynamic } from './shared/fragment.js';
* @param {Context} context * @param {Context} context
*/ */
export function ExpressionTag(node, context) { export function ExpressionTag(node, context) {
const in_attribute = context.path.at(-1)?.type === 'Attribute'; const in_template = context.path.at(-1)?.type === 'Fragment';
if (!in_attribute && context.state.parent_element) { if (in_template && context.state.parent_element) {
if (!is_tag_valid_with_parent('#text', context.state.parent_element)) { const message = is_tag_valid_with_parent('#text', context.state.parent_element);
e.node_invalid_placement(node, '`{expression}`', context.state.parent_element); if (message) {
e.node_invalid_placement(node, message);
} }
} }

@ -26,30 +26,5 @@ export function MemberExpression(node, context) {
context.state.analysis.needs_context = true; context.state.analysis.needs_context = true;
} }
if (context.state.reactive_statement) {
const left = object(node);
if (left !== null) {
const binding = context.state.scope.get(left.name);
if (binding && binding.kind === 'normal') {
const parent = /** @type {Node} */ (context.path.at(-1));
if (
binding.scope === context.state.analysis.module.scope ||
binding.declaration_kind === 'import' ||
(binding.initial &&
binding.initial.type !== 'ArrayExpression' &&
binding.initial.type !== 'ObjectExpression' &&
binding.scope.function_depth <= 1)
) {
if (parent.type !== 'MemberExpression' && parent.type !== 'CallExpression') {
w.reactive_declaration_non_reactive_property(node);
}
}
}
}
}
context.next(); context.next();
} }

@ -114,15 +114,12 @@ export function RegularElement(node, context) {
if (!past_parent) { if (!past_parent) {
if (ancestor.type === 'RegularElement' && ancestor.name === context.state.parent_element) { if (ancestor.type === 'RegularElement' && ancestor.name === context.state.parent_element) {
if (!is_tag_valid_with_parent(node.name, context.state.parent_element)) { const message = is_tag_valid_with_parent(node.name, context.state.parent_element);
if (message) {
if (only_warn) { if (only_warn) {
w.node_invalid_placement_ssr( w.node_invalid_placement_ssr(node, message);
node,
`\`<${node.name}>\``,
context.state.parent_element
);
} else { } else {
e.node_invalid_placement(node, `\`<${node.name}>\``, context.state.parent_element); e.node_invalid_placement(node, message);
} }
} }
@ -131,11 +128,12 @@ export function RegularElement(node, context) {
} else if (ancestor.type === 'RegularElement') { } else if (ancestor.type === 'RegularElement') {
ancestors.push(ancestor.name); ancestors.push(ancestor.name);
if (!is_tag_valid_with_ancestor(node.name, ancestors)) { const message = is_tag_valid_with_ancestor(node.name, ancestors);
if (message) {
if (only_warn) { if (only_warn) {
w.node_invalid_placement_ssr(node, `\`<${node.name}>\``, ancestor.name); w.node_invalid_placement_ssr(node, message);
} else { } else {
e.node_invalid_placement(node, `\`<${node.name}>\``, ancestor.name); e.node_invalid_placement(node, message);
} }
} }
} else if ( } else if (

@ -4,6 +4,7 @@ import { unwrap_optional } from '../../../utils/ast.js';
import * as e from '../../../errors.js'; import * as e from '../../../errors.js';
import { validate_opening_tag } from './shared/utils.js'; import { validate_opening_tag } from './shared/utils.js';
import { mark_subtree_dynamic } from './shared/fragment.js'; import { mark_subtree_dynamic } from './shared/fragment.js';
import { is_resolved_snippet } from './shared/snippets.js';
/** /**
* @param {AST.RenderTag} node * @param {AST.RenderTag} node
@ -13,13 +14,25 @@ export function RenderTag(node, context) {
validate_opening_tag(node, context.state, '@'); validate_opening_tag(node, context.state, '@');
node.metadata.path = [...context.path]; node.metadata.path = [...context.path];
context.state.analysis.elements.push(node);
const callee = unwrap_optional(node.expression).callee; const callee = unwrap_optional(node.expression).callee;
node.metadata.dynamic = const binding = callee.type === 'Identifier' ? context.state.scope.get(callee.name) : null;
callee.type !== 'Identifier' || context.state.scope.get(callee.name)?.kind !== 'normal';
node.metadata.dynamic = binding?.kind !== 'normal';
/**
* If we can't unambiguously resolve this to a declaration, we
* must assume the worst and link the render tag to every snippet
*/
let resolved = callee.type === 'Identifier' && is_resolved_snippet(binding);
if (binding?.initial?.type === 'SnippetBlock') {
// if this render tag unambiguously references a local snippet, our job is easy
node.metadata.snippets.add(binding.initial);
}
context.state.analysis.snippet_renderers.set(node, resolved);
context.state.analysis.uses_render_tags = true; context.state.analysis.uses_render_tags = true;
const raw_args = unwrap_optional(node.expression).arguments; const raw_args = unwrap_optional(node.expression).arguments;

@ -1,4 +1,5 @@
/** @import { AST } from '#compiler' */ /** @import { AST, Binding, SvelteNode } from '#compiler' */
/** @import { Scope } from '../../scope' */
/** @import { Context } from '../types' */ /** @import { Context } from '../types' */
import { validate_block_not_empty, validate_opening_tag } from './shared/utils.js'; import { validate_block_not_empty, validate_opening_tag } from './shared/utils.js';
import * as e from '../../../errors.js'; import * as e from '../../../errors.js';
@ -8,6 +9,8 @@ import * as e from '../../../errors.js';
* @param {Context} context * @param {Context} context
*/ */
export function SnippetBlock(node, context) { export function SnippetBlock(node, context) {
context.state.analysis.snippets.add(node);
validate_block_not_empty(node.body, context); validate_block_not_empty(node.body, context);
if (context.state.analysis.runes) { if (context.state.analysis.runes) {
@ -22,6 +25,25 @@ export function SnippetBlock(node, context) {
context.next({ ...context.state, parent_element: null }); context.next({ ...context.state, parent_element: null });
const can_hoist =
context.path.length === 1 &&
context.path[0].type === 'Fragment' &&
can_hoist_snippet(context.state.scope, context.state.scopes);
const name = node.expression.name;
if (can_hoist) {
const binding = /** @type {Binding} */ (context.state.scope.get(name));
context.state.analysis.module.scope.declarations.set(name, binding);
} else {
const undefined_export = context.state.analysis.undefined_exports.get(name);
if (undefined_export) {
e.snippet_invalid_export(undefined_export);
}
}
node.metadata.can_hoist = can_hoist;
const { path } = context; const { path } = context;
const parent = path.at(-2); const parent = path.at(-2);
if (!parent) return; if (!parent) return;
@ -56,3 +78,35 @@ export function SnippetBlock(node, context) {
} }
} }
} }
/**
* @param {Map<SvelteNode, Scope>} scopes
* @param {Scope} scope
*/
function can_hoist_snippet(scope, scopes, visited = new Set()) {
for (const [reference] of scope.references) {
const binding = scope.get(reference);
if (!binding || binding.scope.function_depth === 0) {
continue;
}
// ignore bindings declared inside the snippet (e.g. the snippet's own parameters)
if (binding.scope.function_depth >= scope.function_depth) {
continue;
}
if (binding.initial?.type === 'SnippetBlock') {
if (visited.has(binding)) continue;
visited.add(binding);
if (can_hoist_snippet(binding.scope, scopes, visited)) {
continue;
}
}
return false;
}
return true;
}

@ -0,0 +1,16 @@
/** @import { SpreadElement } from 'estree' */
/** @import { Context } from '../types' */
/**
* @param {SpreadElement} node
* @param {Context} context
*/
export function SpreadElement(node, context) {
if (context.state.expression) {
// treat e.g. `[...x]` the same as `[...x.values()]`
context.state.expression.has_call = true;
context.state.expression.has_state = true;
}
context.next();
}

@ -0,0 +1,27 @@
/** @import { AST } from '#compiler' */
/** @import { Context } from '../types' */
import * as e from '../../../errors.js';
const valid = ['onerror', 'failed'];
/**
* @param {AST.SvelteBoundary} node
* @param {Context} context
*/
export function SvelteBoundary(node, context) {
for (const attribute of node.attributes) {
if (attribute.type !== 'Attribute' || !valid.includes(attribute.name)) {
e.svelte_boundary_invalid_attribute(attribute);
}
if (
attribute.value === true ||
(Array.isArray(attribute.value) &&
(attribute.value.length !== 1 || attribute.value[0].type !== 'ExpressionTag'))
) {
e.svelte_boundary_invalid_attribute_value(attribute);
}
}
context.next();
}

@ -9,11 +9,12 @@ import * as e from '../../../errors.js';
* @param {Context} context * @param {Context} context
*/ */
export function Text(node, context) { export function Text(node, context) {
const in_attribute = context.path.at(-1)?.type === 'Attribute'; const in_template = context.path.at(-1)?.type === 'Fragment';
if (!in_attribute && context.state.parent_element && regex_not_whitespace.test(node.data)) { if (in_template && context.state.parent_element && regex_not_whitespace.test(node.data)) {
if (!is_tag_valid_with_parent('#text', context.state.parent_element)) { const message = is_tag_valid_with_parent('#text', context.state.parent_element);
e.node_invalid_placement(node, 'Text node', context.state.parent_element); if (message) {
e.node_invalid_placement(node, message);
} }
} }
} }

@ -9,12 +9,63 @@ import {
validate_slot_attribute validate_slot_attribute
} from './attribute.js'; } from './attribute.js';
import { mark_subtree_dynamic } from './fragment.js'; import { mark_subtree_dynamic } from './fragment.js';
import { is_resolved_snippet } from './snippets.js';
/** /**
* @param {AST.Component | AST.SvelteComponent | AST.SvelteSelf} node * @param {AST.Component | AST.SvelteComponent | AST.SvelteSelf} node
* @param {Context} context * @param {Context} context
*/ */
export function visit_component(node, context) { export function visit_component(node, context) {
node.metadata.path = [...context.path];
// link this node to all the snippets that it could render, so that we can prune CSS correctly
node.metadata.snippets = new Set();
// 'resolved' means we know which snippets this component might render. if it is `false`,
// then `node.metadata.snippets` is populated with every locally defined snippet
// once analysis is complete
let resolved = true;
for (const attribute of node.attributes) {
if (attribute.type === 'SpreadAttribute' || attribute.type === 'BindDirective') {
resolved = false;
continue;
}
if (attribute.type !== 'Attribute' || !is_expression_attribute(attribute)) {
continue;
}
const expression = get_attribute_expression(attribute);
// given an attribute like `foo={bar}`, if `bar` resolves to an import or a prop
// then we know it doesn't reference a locally defined snippet. if it resolves
// to a `{#snippet bar()}` then we know _which_ snippet it resolves to. in all
// other cases, we can't know (without much more complex static analysis) which
// snippets the component might render, so we treat the component as unresolved
if (expression.type === 'Identifier') {
const binding = context.state.scope.get(expression.name);
resolved &&= is_resolved_snippet(binding);
if (binding?.initial?.type === 'SnippetBlock') {
node.metadata.snippets.add(binding.initial);
}
} else if (expression.type !== 'Literal') {
resolved = false;
}
}
if (resolved) {
for (const child of node.fragment.nodes) {
if (child.type === 'SnippetBlock') {
node.metadata.snippets.add(child);
}
}
}
context.state.analysis.snippet_renderers.set(node, resolved);
mark_subtree_dynamic(context.path); mark_subtree_dynamic(context.path);
for (const attribute of node.attributes) { for (const attribute of node.attributes) {

@ -0,0 +1,17 @@
/** @import { Binding } from '#compiler' */
/**
* Returns `true` if a binding unambiguously resolves to a specific
* snippet declaration, or is external to the current component
* @param {Binding | null} binding
*/
export function is_resolved_snippet(binding) {
return (
!binding ||
binding.declaration_kind === 'import' ||
binding.kind === 'prop' ||
binding.kind === 'rest_prop' ||
binding.kind === 'bindable_prop' ||
binding?.initial?.type === 'SnippetBlock'
);
}

@ -48,6 +48,7 @@ import { SvelteComponent } from './visitors/SvelteComponent.js';
import { SvelteDocument } from './visitors/SvelteDocument.js'; import { SvelteDocument } from './visitors/SvelteDocument.js';
import { SvelteElement } from './visitors/SvelteElement.js'; import { SvelteElement } from './visitors/SvelteElement.js';
import { SvelteFragment } from './visitors/SvelteFragment.js'; import { SvelteFragment } from './visitors/SvelteFragment.js';
import { SvelteBoundary } from './visitors/SvelteBoundary.js';
import { SvelteHead } from './visitors/SvelteHead.js'; import { SvelteHead } from './visitors/SvelteHead.js';
import { SvelteHTML } from './visitors/SvelteHTML.js'; import { SvelteHTML } from './visitors/SvelteHTML.js';
import { SvelteSelf } from './visitors/SvelteSelf.js'; import { SvelteSelf } from './visitors/SvelteSelf.js';
@ -123,6 +124,7 @@ const visitors = {
SvelteDocument, SvelteDocument,
SvelteElement, SvelteElement,
SvelteFragment, SvelteFragment,
SvelteBoundary,
SvelteHead, SvelteHead,
SvelteHTML, SvelteHTML,
SvelteSelf, SvelteSelf,
@ -165,6 +167,8 @@ export function client_component(analysis, options) {
private_state: new Map(), private_state: new Map(),
transform: {}, transform: {},
in_constructor: false, in_constructor: false,
instance_level_snippets: [],
module_level_snippets: [],
// these are set inside the `Fragment` visitor, and cannot be used until then // these are set inside the `Fragment` visitor, and cannot be used until then
before_init: /** @type {any} */ (null), before_init: /** @type {any} */ (null),
@ -370,7 +374,7 @@ export function client_component(analysis, options) {
...store_setup, ...store_setup,
...legacy_reactive_declarations, ...legacy_reactive_declarations,
...group_binding_declarations, ...group_binding_declarations,
...analysis.top_level_snippets, ...state.instance_level_snippets,
.../** @type {ESTree.Statement[]} */ (instance.body), .../** @type {ESTree.Statement[]} */ (instance.body),
analysis.runes || !analysis.needs_context analysis.runes || !analysis.needs_context
? b.empty ? b.empty
@ -485,7 +489,7 @@ export function client_component(analysis, options) {
} }
} }
body = [...imports, ...body]; body = [...imports, ...state.module_level_snippets, ...body];
const component = b.function_declaration( const component = b.function_declaration(
b.id(analysis.name), b.id(analysis.name),

@ -6,7 +6,8 @@ import type {
PrivateIdentifier, PrivateIdentifier,
Expression, Expression,
AssignmentExpression, AssignmentExpression,
UpdateExpression UpdateExpression,
VariableDeclaration
} from 'estree'; } from 'estree';
import type { Namespace, SvelteNode, ValidatedCompileOptions } from '#compiler'; import type { Namespace, SvelteNode, ValidatedCompileOptions } from '#compiler';
import type { TransformState } from '../types.js'; import type { TransformState } from '../types.js';
@ -31,7 +32,7 @@ export interface ClientTransformState extends TransformState {
/** turn `foo = bar` into e.g. `$.set(foo, bar)` */ /** turn `foo = bar` into e.g. `$.set(foo, bar)` */
assign?: (node: Identifier, value: Expression) => Expression; assign?: (node: Identifier, value: Expression) => Expression;
/** turn `foo.bar = baz` into e.g. `$.mutate(foo, $.get(foo).bar = baz);` */ /** turn `foo.bar = baz` into e.g. `$.mutate(foo, $.get(foo).bar = baz);` */
mutate?: (node: Identifier, mutation: AssignmentExpression) => Expression; mutate?: (node: Identifier, mutation: AssignmentExpression | UpdateExpression) => Expression;
/** turn `foo++` into e.g. `$.update(foo)` */ /** turn `foo++` into e.g. `$.update(foo)` */
update?: (node: UpdateExpression) => Expression; update?: (node: UpdateExpression) => Expression;
} }
@ -85,6 +86,11 @@ export interface ComponentClientTransformState extends ClientTransformState {
/** The $: calls, which will be ordered in the end */ /** The $: calls, which will be ordered in the end */
readonly legacy_reactive_statements: Map<LabeledStatement, Statement>; readonly legacy_reactive_statements: Map<LabeledStatement, Statement>;
/** Snippets hoisted to the instance */
readonly instance_level_snippets: VariableDeclaration[];
/** Snippets hoisted to the module */
readonly module_level_snippets: VariableDeclaration[];
} }
export interface StateField { export interface StateField {

@ -1,8 +1,14 @@
/** @import { AssignmentExpression, AssignmentOperator, Expression, Pattern } from 'estree' */ /** @import { Location } from 'locate-character' */
/** @import { AssignmentExpression, AssignmentOperator, Expression, Identifier, Literal, MemberExpression, Pattern } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { Context } from '../types.js' */ /** @import { Context } from '../types.js' */
import * as b from '../../../../utils/builders.js'; import * as b from '../../../../utils/builders.js';
import { build_assignment_value } from '../../../../utils/ast.js'; import {
import { is_ignored } from '../../../../state.js'; build_assignment_value,
get_attribute_expression,
is_event_attribute
} from '../../../../utils/ast.js';
import { dev, filename, is_ignored, locator } from '../../../../state.js';
import { build_proxy_reassignment, should_proxy } from '../utils.js'; import { build_proxy_reassignment, should_proxy } from '../utils.js';
import { visit_assignment_expression } from '../../shared/assignments.js'; import { visit_assignment_expression } from '../../shared/assignments.js';
@ -20,6 +26,24 @@ export function AssignmentExpression(node, context) {
: expression; : expression;
} }
/**
* Determines whether the value will be coerced on assignment (as with e.g. `+=`).
* If not, we may need to proxify the value, or warn that the value will not be
* proxified in time
* @param {AssignmentOperator} operator
*/
function is_non_coercive_operator(operator) {
return ['=', '||=', '&&=', '??='].includes(operator);
}
/** @type {Record<string, string>} */
const callees = {
'=': '$.assign',
'&&=': '$.assign_and',
'||=': '$.assign_or',
'??=': '$.assign_nullish'
};
/** /**
* @param {AssignmentOperator} operator * @param {AssignmentOperator} operator
* @param {Pattern} left * @param {Pattern} left
@ -41,7 +65,11 @@ function build_assignment(operator, left, right, context) {
context.visit(build_assignment_value(operator, left, right)) context.visit(build_assignment_value(operator, left, right))
); );
if (private_state.kind !== 'raw_state' && should_proxy(value, context.state.scope)) { if (
private_state.kind === 'state' &&
is_non_coercive_operator(operator) &&
should_proxy(value, context.state.scope)
) {
value = build_proxy_reassignment(value, b.member(b.this, private_state.id)); value = build_proxy_reassignment(value, b.member(b.this, private_state.id));
} }
@ -73,24 +101,28 @@ function build_assignment(operator, left, right, context) {
? context.state.transform[object.name] ? context.state.transform[object.name]
: null; : null;
const path = context.path.map((node) => node.type);
// reassignment // reassignment
if (object === left && transform?.assign) { if (object === left && transform?.assign) {
// special case — if an element binding, we know it's a primitive
const is_primitive = path.at(-1) === 'BindDirective' && path.at(-2) === 'RegularElement';
let value = /** @type {Expression} */ ( let value = /** @type {Expression} */ (
context.visit(build_assignment_value(operator, left, right)) context.visit(build_assignment_value(operator, left, right))
); );
// special case — if an element binding, we know it's a primitive
const path = context.path.map((node) => node.type);
const is_primitive = path.at(-1) === 'BindDirective' && path.at(-2) === 'RegularElement';
if ( if (
!is_primitive && !is_primitive &&
binding.kind !== 'prop' && binding.kind !== 'prop' &&
binding.kind !== 'bindable_prop' && binding.kind !== 'bindable_prop' &&
binding.kind !== 'raw_state' &&
context.state.analysis.runes && context.state.analysis.runes &&
should_proxy(value, context.state.scope) should_proxy(right, context.state.scope) &&
is_non_coercive_operator(operator)
) { ) {
value = binding.kind === 'raw_state' ? value : build_proxy_reassignment(value, object); value = build_proxy_reassignment(value, object);
} }
return transform.assign(object, value); return transform.assign(object, value);
@ -108,5 +140,57 @@ function build_assignment(operator, left, right, context) {
); );
} }
// in cases like `(object.items ??= []).push(value)`, we may need to warn
// if the value gets proxified, since the proxy _isn't_ the thing that
// will be pushed to. we do this by transforming it to something like
// `$.assign_nullish(object, 'items', [])`
let should_transform =
dev && path.at(-1) !== 'ExpressionStatement' && is_non_coercive_operator(operator);
// special case — ignore `onclick={() => (...)}`
if (
path.at(-1) === 'ArrowFunctionExpression' &&
(path.at(-2) === 'RegularElement' || path.at(-2) === 'SvelteElement')
) {
const element = /** @type {AST.RegularElement} */ (context.path.at(-2));
const attribute = element.attributes.find((attribute) => {
if (attribute.type !== 'Attribute' || !is_event_attribute(attribute)) {
return false;
}
const expression = get_attribute_expression(attribute);
return expression === context.path.at(-1);
});
if (attribute) {
should_transform = false;
}
}
if (left.type === 'MemberExpression' && should_transform) {
const callee = callees[operator];
const loc = /** @type {Location} */ (locator(/** @type {number} */ (left.start)));
const location = `${filename}:${loc.line}:${loc.column}`;
return /** @type {Expression} */ (
context.visit(
b.call(
callee,
/** @type {Expression} */ (left.object),
/** @type {Expression} */ (
left.computed
? left.property
: b.literal(/** @type {Identifier} */ (left.property).name)
),
right,
b.literal(location)
)
)
);
}
return null; return null;
} }

@ -184,7 +184,12 @@ export function ClassBody(node, context) {
'method', 'method',
b.id('$.ADD_OWNER'), b.id('$.ADD_OWNER'),
[b.id('owner')], [b.id('owner')],
Array.from(public_state.keys()).map((name) => Array.from(public_state)
// Only run ownership addition on $state fields.
// Theoretically someone could create a `$state` while creating `$state.raw` or inside a `$derived.by`,
// but that feels so much of an edge case that it doesn't warrant a perf hit for the common case.
.filter(([_, { kind }]) => kind === 'state')
.map(([name]) =>
b.stmt( b.stmt(
b.call( b.call(
'$.add_owner', '$.add_owner',

@ -47,8 +47,8 @@ export function EachBlock(node, context) {
const key_is_item = const key_is_item =
node.key?.type === 'Identifier' && node.key?.type === 'Identifier' &&
node.context.type === 'Identifier' && node.context?.type === 'Identifier' &&
node.context.name === node.key.name; node.context?.name === node.key.name;
// if the each block expression references a store subscription, we need // if the each block expression references a store subscription, we need
// to use mutable stores internally // to use mutable stores internally
@ -147,7 +147,7 @@ export function EachBlock(node, context) {
// which needs a reference to the index // which needs a reference to the index
const index = const index =
each_node_meta.contains_group_binding || !node.index ? each_node_meta.index : b.id(node.index); each_node_meta.contains_group_binding || !node.index ? each_node_meta.index : b.id(node.index);
const item = node.context.type === 'Identifier' ? node.context : b.id('$$item'); const item = node.context?.type === 'Identifier' ? node.context : b.id('$$item');
let uses_index = each_node_meta.contains_group_binding; let uses_index = each_node_meta.contains_group_binding;
let key_uses_index = false; let key_uses_index = false;
@ -185,7 +185,7 @@ export function EachBlock(node, context) {
if (!context.state.analysis.runes) sequence.push(invalidate); if (!context.state.analysis.runes) sequence.push(invalidate);
if (invalidate_store) sequence.push(invalidate_store); if (invalidate_store) sequence.push(invalidate_store);
if (node.context.type === 'Identifier') { if (node.context?.type === 'Identifier') {
const binding = /** @type {Binding} */ (context.state.scope.get(node.context.name)); const binding = /** @type {Binding} */ (context.state.scope.get(node.context.name));
child_state.transform[node.context.name] = { child_state.transform[node.context.name] = {
@ -214,11 +214,14 @@ export function EachBlock(node, context) {
return b.sequence([b.assignment('=', left, value), ...sequence]); return b.sequence([b.assignment('=', left, value), ...sequence]);
}, },
mutate: (_, mutation) => b.sequence([mutation, ...sequence]) mutate: (_, mutation) => {
uses_index = true;
return b.sequence([mutation, ...sequence]);
}
}; };
delete key_state.transform[node.context.name]; delete key_state.transform[node.context.name];
} else { } else if (node.context) {
const unwrapped = (flags & EACH_ITEM_REACTIVE) !== 0 ? b.call('$.get', item) : item; const unwrapped = (flags & EACH_ITEM_REACTIVE) !== 0 ? b.call('$.get', item) : item;
for (const path of extract_paths(node.context)) { for (const path of extract_paths(node.context)) {
@ -260,11 +263,12 @@ export function EachBlock(node, context) {
let key_function = b.id('$.index'); let key_function = b.id('$.index');
if (node.metadata.keyed) { if (node.metadata.keyed) {
const pattern = /** @type {Pattern} */ (node.context); // can only be keyed when a context is provided
const expression = /** @type {Expression} */ ( const expression = /** @type {Expression} */ (
context.visit(/** @type {Expression} */ (node.key), key_state) context.visit(/** @type {Expression} */ (node.key), key_state)
); );
key_function = b.arrow(key_uses_index ? [node.context, index] : [node.context], expression); key_function = b.arrow(key_uses_index ? [pattern, index] : [pattern], expression);
} }
if (node.index && each_node_meta.contains_group_binding) { if (node.index && each_node_meta.contains_group_binding) {

@ -9,23 +9,44 @@ import * as b from '../../../../utils/builders.js';
*/ */
export function IfBlock(node, context) { export function IfBlock(node, context) {
context.state.template.push('<!>'); context.state.template.push('<!>');
const statements = [];
const consequent = /** @type {BlockStatement} */ (context.visit(node.consequent)); const consequent = /** @type {BlockStatement} */ (context.visit(node.consequent));
const consequent_id = context.state.scope.generate('consequent');
statements.push(b.var(b.id(consequent_id), b.arrow([b.id('$$anchor')], consequent)));
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)));
}
/** @type {Expression[]} */
const args = [ const args = [
context.state.node, context.state.node,
b.thunk(/** @type {Expression} */ (context.visit(node.test))), b.arrow(
b.arrow([b.id('$$anchor')], consequent) [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
)
])
)
]; ];
if (node.alternate || node.elseif) {
args.push(
node.alternate
? b.arrow([b.id('$$anchor')], /** @type {BlockStatement} */ (context.visit(node.alternate)))
: b.literal(null)
);
}
if (node.elseif) { if (node.elseif) {
// We treat this... // We treat this...
// //
@ -51,5 +72,7 @@ export function IfBlock(node, context) {
args.push(b.literal(true)); args.push(b.literal(true));
} }
context.state.init.push(b.stmt(b.call('$.if', ...args))); statements.push(b.stmt(b.call('$.if', ...args)));
context.state.init.push(b.block(statements));
} }

@ -1,6 +1,8 @@
/** @import { Location } from 'locate-character' */
/** @import { Expression, LabeledStatement, Statement } from 'estree' */ /** @import { Expression, LabeledStatement, Statement } from 'estree' */
/** @import { ReactiveStatement } from '#compiler' */ /** @import { ReactiveStatement } from '#compiler' */
/** @import { ComponentContext } from '../types' */ /** @import { ComponentContext } from '../types' */
import { dev, is_ignored, locator } from '../../../../state.js';
import * as b from '../../../../utils/builders.js'; import * as b from '../../../../utils/builders.js';
import { build_getter } from '../utils.js'; import { build_getter } from '../utils.js';
@ -48,6 +50,11 @@ export function LabeledStatement(node, context) {
sequence.push(serialized); sequence.push(serialized);
} }
const location =
dev && !is_ignored(node, 'reactive_declaration_non_reactive_property')
? locator(/** @type {number} */ (node.start))
: undefined;
// these statements will be topologically ordered later // these statements will be topologically ordered later
context.state.legacy_reactive_statements.set( context.state.legacy_reactive_statements.set(
node, node,
@ -55,7 +62,9 @@ export function LabeledStatement(node, context) {
b.call( b.call(
'$.legacy_pre_effect', '$.legacy_pre_effect',
sequence.length > 0 ? b.thunk(b.sequence(sequence)) : b.thunk(b.block([])), sequence.length > 0 ? b.thunk(b.sequence(sequence)) : b.thunk(b.block([])),
b.thunk(b.block(body)) b.thunk(b.block(body)),
location && b.literal(location.line),
location && b.literal(location.column)
) )
) )
); );

@ -77,13 +77,15 @@ export function Program(_, context) {
return b.call( return b.call(
'$.store_mutate', '$.store_mutate',
get_store(), get_store(),
b.assignment( mutation.type === 'AssignmentExpression'
? b.assignment(
mutation.operator, mutation.operator,
/** @type {MemberExpression} */ ( /** @type {MemberExpression} */ (
replace(/** @type {MemberExpression} */ (mutation.left)) replace(/** @type {MemberExpression} */ (mutation.left))
), ),
mutation.right mutation.right
), )
: b.update(mutation.operator, replace(mutation.argument), mutation.prefix),
untracked untracked
); );
}, },

@ -172,21 +172,29 @@ export function RegularElement(node, context) {
} }
} }
if (node.name === 'input') {
const has_value_attribute = attributes.some(
(attribute) =>
attribute.type === 'Attribute' &&
(attribute.name === 'value' || attribute.name === 'checked') &&
!is_text_attribute(attribute)
);
const has_default_value_attribute = attributes.some(
(attribute) =>
attribute.type === 'Attribute' &&
(attribute.name === 'defaultValue' || attribute.name === 'defaultChecked')
);
if ( if (
node.name === 'input' && !has_default_value_attribute &&
(has_spread || (has_spread ||
bindings.has('value') || bindings.has('value') ||
bindings.has('checked') || bindings.has('checked') ||
bindings.has('group') || bindings.has('group') ||
attributes.some( (!bindings.has('group') && has_value_attribute))
(attribute) =>
attribute.type === 'Attribute' &&
(attribute.name === 'value' || attribute.name === 'checked') &&
!is_text_attribute(attribute)
))
) { ) {
context.state.init.push(b.stmt(b.call('$.remove_input_defaults', context.state.node))); context.state.init.push(b.stmt(b.call('$.remove_input_defaults', context.state.node)));
} }
}
if (node.name === 'textarea') { if (node.name === 'textarea') {
const attribute = lookup.get('value') ?? lookup.get('checked'); const attribute = lookup.get('value') ?? lookup.get('checked');
@ -283,7 +291,7 @@ export function RegularElement(node, context) {
const is = is_custom_element const is = is_custom_element
? build_custom_element_attribute_update_assignment(node_id, attribute, context) ? build_custom_element_attribute_update_assignment(node_id, attribute, context)
: build_element_attribute_update_assignment(node, node_id, attribute, context); : build_element_attribute_update_assignment(node, node_id, attribute, attributes, context);
if (is) is_attributes_reactive = true; if (is) is_attributes_reactive = true;
} }
} }
@ -511,10 +519,17 @@ function setup_select_synchronization(value_binding, context) {
* @param {AST.RegularElement} element * @param {AST.RegularElement} element
* @param {Identifier} node_id * @param {Identifier} node_id
* @param {AST.Attribute} attribute * @param {AST.Attribute} attribute
* @param {Array<AST.Attribute | AST.SpreadAttribute>} attributes
* @param {ComponentContext} context * @param {ComponentContext} context
* @returns {boolean} * @returns {boolean}
*/ */
function build_element_attribute_update_assignment(element, node_id, attribute, context) { function build_element_attribute_update_assignment(
element,
node_id,
attribute,
attributes,
context
) {
const state = context.state; const state = context.state;
const name = get_attribute_name(element, attribute); const name = get_attribute_name(element, attribute);
const is_svg = context.state.metadata.namespace === 'svg' || element.name === 'svg'; const is_svg = context.state.metadata.namespace === 'svg' || element.name === 'svg';
@ -555,6 +570,28 @@ function build_element_attribute_update_assignment(element, node_id, attribute,
update = b.stmt(b.call('$.set_value', node_id, value)); update = b.stmt(b.call('$.set_value', node_id, value));
} else if (name === 'checked') { } else if (name === 'checked') {
update = b.stmt(b.call('$.set_checked', node_id, value)); 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 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
// current value, which is not what we want, which is why we need to do some extra work.
name === 'defaultValue' &&
(attributes.some(
(attr) => attr.type === 'Attribute' && attr.name === 'value' && is_text_attribute(attr)
) ||
(element.name === 'textarea' && element.fragment.nodes.length > 0))
) {
update = b.stmt(b.call('$.set_default_value', node_id, value));
} else 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)) { } else if (is_dom_property(name)) {
update = b.stmt(b.assignment('=', b.member(node_id, name), value)); update = b.stmt(b.assignment('=', b.member(node_id, name), value));
} else { } else {

@ -83,7 +83,11 @@ export function SnippetBlock(node, context) {
// Top-level snippets are hoisted so they can be referenced in the `<script>` // Top-level snippets are hoisted so they can be referenced in the `<script>`
if (context.path.length === 1 && context.path[0].type === 'Fragment') { if (context.path.length === 1 && context.path[0].type === 'Fragment') {
context.state.analysis.top_level_snippets.push(declaration); if (node.metadata.can_hoist) {
context.state.module_level_snippets.push(declaration);
} else {
context.state.instance_level_snippets.push(declaration);
}
} else { } else {
context.state.init.push(declaration); context.state.init.push(declaration);
} }

@ -0,0 +1,61 @@
/** @import { BlockStatement, Statement, Expression } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */
import * as b from '../../../../utils/builders.js';
/**
* @param {AST.SvelteBoundary} node
* @param {ComponentContext} context
*/
export function SvelteBoundary(node, context) {
const props = b.object([]);
for (const attribute of node.attributes) {
if (attribute.type !== 'Attribute' || attribute.value === true) {
// these can't exist, because they would have caused validation
// to fail, but typescript doesn't know that
continue;
}
const chunk = Array.isArray(attribute.value)
? /** @type {AST.ExpressionTag} */ (attribute.value[0])
: attribute.value;
const expression = /** @type {Expression} */ (context.visit(chunk.expression, context.state));
if (attribute.metadata.expression.has_state) {
props.properties.push(b.get(attribute.name, [b.return(expression)]));
} else {
props.properties.push(b.init(attribute.name, expression));
}
}
const nodes = [];
/** @type {Statement[]} */
const snippet_statements = [];
// Capture the `failed` implicit snippet prop
for (const child of node.fragment.nodes) {
if (child.type === 'SnippetBlock' && child.expression.name === 'failed') {
/** @type {Statement[]} */
const init = [];
context.visit(child, { ...context.state, init });
props.properties.push(b.prop('init', child.expression, child.expression));
snippet_statements.push(...init);
} else {
nodes.push(child);
}
}
const block = /** @type {BlockStatement} */ (context.visit({ ...node.fragment, nodes }));
const boundary = b.stmt(
b.call('$.boundary', context.state.node, props, b.arrow([b.id('$$anchor')], block))
);
context.state.template.push('<!>');
context.state.init.push(
snippet_statements.length > 0 ? b.block([...snippet_statements, boundary]) : boundary
);
}

@ -1,4 +1,4 @@
/** @import { Expression, Node, Pattern, Statement, UpdateExpression } from 'estree' */ /** @import { AssignmentExpression, Expression, UpdateExpression } from 'estree' */
/** @import { Context } from '../types' */ /** @import { Context } from '../types' */
import { is_ignored } from '../../../../state.js'; import { is_ignored } from '../../../../state.js';
import { object } from '../../../../utils/ast.js'; import { object } from '../../../../utils/ast.js';
@ -34,34 +34,22 @@ export function UpdateExpression(node, context) {
} }
const left = object(argument); const left = object(argument);
if (left === null) return context.next(); const transformers = left && context.state.transform[left.name];
if (left === argument) { if (left === argument && transformers?.update) {
const transform = context.state.transform; // we don't need to worry about ownership_invalid_mutation here, because
const update = transform[left.name]?.update; // we're not mutating but reassigning
return transformers.update(node);
if (update && Object.hasOwn(transform, left.name)) {
return update(node);
}
} }
const assignment = /** @type {Expression} */ ( let update = /** @type {Expression} */ (context.next());
context.visit(
b.assignment(
node.operator === '++' ? '+=' : '-=',
/** @type {Pattern} */ (argument),
b.literal(1)
)
)
);
const parent = /** @type {Node} */ (context.path.at(-1));
const is_standalone = parent.type === 'ExpressionStatement'; // TODO and possibly others, but not e.g. the `test` of a WhileStatement
const update = if (left && transformers?.mutate) {
node.prefix || is_standalone update = transformers.mutate(
? assignment left,
: b.binary(node.operator === '++' ? '-' : '+', assignment, b.literal(1)); /** @type {AssignmentExpression | UpdateExpression} */ (update)
);
}
return is_ignored(node, 'ownership_invalid_mutation') return is_ignored(node, 'ownership_invalid_mutation')
? b.call('$.skip_ownership_validation', b.thunk(update)) ? b.call('$.skip_ownership_validation', b.thunk(update))

@ -20,7 +20,10 @@ export function UseDirective(node, context) {
context.state.node, context.state.node,
b.arrow( b.arrow(
params, params,
b.call(/** @type {Expression} */ (context.visit(parse_directive_name(node.name))), ...params) b.maybe_call(
/** @type {Expression} */ (context.visit(parse_directive_name(node.name))),
...params
)
) )
]; ];

@ -2,7 +2,7 @@
/** @import { AST, TemplateNode } from '#compiler' */ /** @import { AST, TemplateNode } from '#compiler' */
/** @import { ComponentContext } from '../../types.js' */ /** @import { ComponentContext } from '../../types.js' */
import { dev, is_ignored } from '../../../../../state.js'; import { dev, is_ignored } from '../../../../../state.js';
import { get_attribute_chunks } from '../../../../../utils/ast.js'; import { get_attribute_chunks, object } from '../../../../../utils/ast.js';
import * as b from '../../../../../utils/builders.js'; import * as b from '../../../../../utils/builders.js';
import { create_derived } from '../../utils.js'; import { create_derived } from '../../utils.js';
import { build_bind_this, validate_binding } from '../shared/utils.js'; import { build_bind_this, validate_binding } from '../shared/utils.js';
@ -20,6 +20,8 @@ import { determine_slot } from '../../../../../utils/slot.js';
export function build_component(node, component_name, context, anchor = context.state.node) { export function build_component(node, component_name, context, anchor = context.state.node) {
/** @type {Array<Property[] | Expression>} */ /** @type {Array<Property[] | Expression>} */
const props_and_spreads = []; const props_and_spreads = [];
/** @type {Array<() => void>} */
const delayed_props = [];
/** @type {ExpressionStatement[]} */ /** @type {ExpressionStatement[]} */
const lets = []; const lets = [];
@ -63,8 +65,10 @@ export function build_component(node, component_name, context, anchor = context.
/** /**
* @param {Property} prop * @param {Property} prop
* @param {boolean} [delay]
*/ */
function push_prop(prop) { function push_prop(prop, delay = false) {
const do_push = () => {
const current = props_and_spreads.at(-1); const current = props_and_spreads.at(-1);
const current_is_props = Array.isArray(current); const current_is_props = Array.isArray(current);
const props = current_is_props ? current : []; const props = current_is_props ? current : [];
@ -72,6 +76,13 @@ export function build_component(node, component_name, context, anchor = context.
if (!current_is_props) { if (!current_is_props) {
props_and_spreads.push(props); props_and_spreads.push(props);
} }
};
if (delay) {
delayed_props.push(do_push);
} else {
do_push();
}
} }
if (slot_scope_applies_to_itself) { if (slot_scope_applies_to_itself) {
@ -176,6 +187,15 @@ export function build_component(node, component_name, context, anchor = context.
bind_this = attribute.expression; bind_this = attribute.expression;
} else { } else {
if (dev) { if (dev) {
const left = object(attribute.expression);
let binding;
if (left?.type === 'Identifier') {
binding = context.state.scope.get(left.name);
}
// Only run ownership addition on $state fields.
// Theoretically someone could create a `$state` while creating `$state.raw` or inside a `$derived.by`,
// but that feels so much of an edge case that it doesn't warrant a perf hit for the common case.
if (binding?.kind !== 'derived' && binding?.kind !== 'raw_state') {
binding_initializers.push( binding_initializers.push(
b.stmt( b.stmt(
b.call( b.call(
@ -187,27 +207,33 @@ export function build_component(node, component_name, context, anchor = context.
) )
); );
} }
}
const is_store_sub = const is_store_sub =
attribute.expression.type === 'Identifier' && attribute.expression.type === 'Identifier' &&
context.state.scope.get(attribute.expression.name)?.kind === 'store_sub'; context.state.scope.get(attribute.expression.name)?.kind === 'store_sub';
// Delay prop pushes so bindings come at the end, to avoid spreads overwriting them
if (is_store_sub) { if (is_store_sub) {
push_prop( push_prop(
b.get(attribute.name, [b.stmt(b.call('$.mark_store_binding')), b.return(expression)]) b.get(attribute.name, [b.stmt(b.call('$.mark_store_binding')), b.return(expression)]),
true
); );
} else { } else {
push_prop(b.get(attribute.name, [b.return(expression)])); push_prop(b.get(attribute.name, [b.return(expression)]), true);
} }
const assignment = b.assignment('=', attribute.expression, b.id('$$value')); const assignment = b.assignment('=', attribute.expression, b.id('$$value'));
push_prop( push_prop(
b.set(attribute.name, [b.stmt(/** @type {Expression} */ (context.visit(assignment)))]) b.set(attribute.name, [b.stmt(/** @type {Expression} */ (context.visit(assignment)))]),
true
); );
} }
} }
} }
delayed_props.forEach((fn) => fn());
if (slot_scope_applies_to_itself) { if (slot_scope_applies_to_itself) {
context.state.init.push(...lets); context.state.init.push(...lets);
} }

@ -39,6 +39,7 @@ import { SvelteSelf } from './visitors/SvelteSelf.js';
import { TitleElement } from './visitors/TitleElement.js'; import { TitleElement } from './visitors/TitleElement.js';
import { UpdateExpression } from './visitors/UpdateExpression.js'; import { UpdateExpression } from './visitors/UpdateExpression.js';
import { VariableDeclaration } from './visitors/VariableDeclaration.js'; import { VariableDeclaration } from './visitors/VariableDeclaration.js';
import { SvelteBoundary } from './visitors/SvelteBoundary.js';
/** @type {Visitors} */ /** @type {Visitors} */
const global_visitors = { const global_visitors = {
@ -77,7 +78,8 @@ const template_visitors = {
SvelteHead, SvelteHead,
SvelteHTML, SvelteHTML,
SvelteSelf, SvelteSelf,
TitleElement TitleElement,
SvelteBoundary
}; };
/** /**

@ -21,7 +21,11 @@ export function EachBlock(node, context) {
state.init.push(b.const(array_id, b.call('$.ensure_array_like', collection))); state.init.push(b.const(array_id, b.call('$.ensure_array_like', collection)));
/** @type {Statement[]} */ /** @type {Statement[]} */
const each = [b.let(/** @type {Pattern} */ (node.context), b.member(array_id, index, true))]; const each = [];
if (node.context) {
each.push(b.let(node.context, b.member(array_id, index, true)));
}
if (index.name !== node.index && node.index != null) { if (index.name !== node.index && node.index != null) {
each.push(b.let(node.index, index)); each.push(b.let(node.index, index));

@ -17,6 +17,9 @@ export function SnippetBlock(node, context) {
// @ts-expect-error - TODO remove this hack once $$render_inner for legacy bindings is gone // @ts-expect-error - TODO remove this hack once $$render_inner for legacy bindings is gone
fn.___snippet = true; fn.___snippet = true;
// TODO hoist where possible if (node.metadata.can_hoist) {
context.state.hoisted.push(fn);
} else {
context.state.init.push(fn); context.state.init.push(fn);
}
} }

@ -0,0 +1,17 @@
/** @import { BlockStatement } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */
import { BLOCK_CLOSE, BLOCK_OPEN } from '../../../../../internal/server/hydration.js';
import * as b from '../../../../utils/builders.js';
/**
* @param {AST.SvelteBoundary} node
* @param {ComponentContext} context
*/
export function SvelteBoundary(node, context) {
context.state.template.push(
b.literal(BLOCK_OPEN),
/** @type {BlockStatement} */ (context.visit(node.fragment)),
b.literal(BLOCK_CLOSE)
);
}

@ -14,13 +14,16 @@ import { build_template } from './shared/utils.js';
*/ */
export function SvelteElement(node, context) { export function SvelteElement(node, context) {
let tag = /** @type {Expression} */ (context.visit(node.tag)); let tag = /** @type {Expression} */ (context.visit(node.tag));
if (dev) {
// Ensure getters/function calls aren't called multiple times.
// If we ever start referencing `tag` more than once in prod, move this out of the if block.
if (tag.type !== 'Identifier') { if (tag.type !== 'Identifier') {
const tag_id = context.state.scope.generate('$$tag'); const tag_id = context.state.scope.generate('$$tag');
context.state.init.push(b.const(tag_id, tag)); context.state.init.push(b.const(tag_id, tag));
tag = b.id(tag_id); tag = b.id(tag_id);
} }
if (dev) {
if (node.fragment.nodes.length > 0) { if (node.fragment.nodes.length > 0) {
context.state.init.push(b.stmt(b.call('$.validate_void_dynamic_element', b.thunk(tag)))); context.state.init.push(b.stmt(b.call('$.validate_void_dynamic_element', b.thunk(tag))));
} }

@ -13,6 +13,8 @@ import { is_element_node } from '../../../../nodes.js';
export function build_inline_component(node, expression, context) { export function build_inline_component(node, expression, context) {
/** @type {Array<Property[] | Expression>} */ /** @type {Array<Property[] | Expression>} */
const props_and_spreads = []; const props_and_spreads = [];
/** @type {Array<() => void>} */
const delayed_props = [];
/** @type {Property[]} */ /** @type {Property[]} */
const custom_css_props = []; const custom_css_props = [];
@ -49,8 +51,10 @@ export function build_inline_component(node, expression, context) {
/** /**
* @param {Property} prop * @param {Property} prop
* @param {boolean} [delay]
*/ */
function push_prop(prop) { function push_prop(prop, delay = false) {
const do_push = () => {
const current = props_and_spreads.at(-1); const current = props_and_spreads.at(-1);
const current_is_props = Array.isArray(current); const current_is_props = Array.isArray(current);
const props = current_is_props ? current : []; const props = current_is_props ? current : [];
@ -58,6 +62,13 @@ export function build_inline_component(node, expression, context) {
if (!current_is_props) { if (!current_is_props) {
props_and_spreads.push(props); props_and_spreads.push(props);
} }
};
if (delay) {
delayed_props.push(do_push);
} else {
do_push();
}
} }
for (const attribute of node.attributes) { for (const attribute of node.attributes) {
@ -81,11 +92,12 @@ export function build_inline_component(node, expression, context) {
const value = build_attribute_value(attribute.value, context, false, true); const value = build_attribute_value(attribute.value, context, false, true);
push_prop(b.prop('init', b.key(attribute.name), value)); push_prop(b.prop('init', b.key(attribute.name), value));
} else if (attribute.type === 'BindDirective' && attribute.name !== 'this') { } else if (attribute.type === 'BindDirective' && attribute.name !== 'this') {
// TODO this needs to turn the whole thing into a while loop because the binding could be mutated eagerly in the child // Delay prop pushes so bindings come at the end, to avoid spreads overwriting them
push_prop( push_prop(
b.get(attribute.name, [ b.get(attribute.name, [
b.return(/** @type {Expression} */ (context.visit(attribute.expression))) b.return(/** @type {Expression} */ (context.visit(attribute.expression)))
]) ]),
true
); );
push_prop( push_prop(
b.set(attribute.name, [ b.set(attribute.name, [
@ -95,11 +107,14 @@ export function build_inline_component(node, expression, context) {
) )
), ),
b.stmt(b.assignment('=', b.id('$$settled'), b.false)) b.stmt(b.assignment('=', b.id('$$settled'), b.false))
]) ]),
true
); );
} }
} }
delayed_props.forEach((fn) => fn());
/** @type {Statement[]} */ /** @type {Statement[]} */
const snippet_declarations = []; const snippet_declarations = [];

@ -82,7 +82,8 @@ export function build_element_attributes(node, context) {
) { ) {
events_to_capture.add(attribute.name); events_to_capture.add(attribute.name);
} }
} else { // the defaultValue/defaultChecked properties don't exist as attributes
} else if (attribute.name !== 'defaultValue' && attribute.name !== 'defaultChecked') {
if (attribute.name === 'class') { if (attribute.name === 'class') {
class_index = attributes.length; class_index = attributes.length;
} else if (attribute.name === 'style') { } else if (attribute.name === 'style') {

@ -306,6 +306,7 @@ export function clean_nodes(
parent.type === 'SnippetBlock' || parent.type === 'SnippetBlock' ||
parent.type === 'EachBlock' || parent.type === 'EachBlock' ||
parent.type === 'SvelteComponent' || parent.type === 'SvelteComponent' ||
parent.type === 'SvelteBoundary' ||
parent.type === 'Component' || parent.type === 'Component' ||
parent.type === 'SvelteSelf') && parent.type === 'SvelteSelf') &&
first && first &&

@ -527,6 +527,7 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
const scope = state.scope.child(); const scope = state.scope.child();
scopes.set(node, scope); scopes.set(node, scope);
if (node.context) {
// declarations // declarations
for (const id of extract_identifiers(node.context)) { for (const id of extract_identifiers(node.context)) {
const binding = scope.declare(id, 'each', 'const'); const binding = scope.declare(id, 'each', 'const');
@ -552,6 +553,7 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
// Visit to pick up references from default initializers // Visit to pick up references from default initializers
visit(node.context, { scope }); visit(node.context, { scope });
}
if (node.index) { if (node.index) {
const is_keyed = const is_keyed =

@ -1,5 +1,5 @@
import type { AST, Binding, Css, SvelteNode } from '#compiler'; import type { AST, Binding, Css, SvelteNode } from '#compiler';
import type { Identifier, LabeledStatement, Program, VariableDeclaration } from 'estree'; import type { Identifier, LabeledStatement, Node, Program } from 'estree';
import type { Scope, ScopeRoot } from './scope.js'; import type { Scope, ScopeRoot } from './scope.js';
export interface Js { export interface Js {
@ -37,7 +37,7 @@ export interface ComponentAnalysis extends Analysis {
instance: Js; instance: Js;
template: Template; template: Template;
/** Used for CSS pruning and scoping */ /** Used for CSS pruning and scoping */
elements: Array<AST.RegularElement | AST.SvelteElement | AST.RenderTag>; elements: Array<AST.RegularElement | AST.SvelteElement>;
runes: boolean; runes: boolean;
exports: Array<{ name: string; alias: string | null }>; exports: Array<{ name: string; alias: string | null }>;
/** Whether the component uses `$$props` */ /** Whether the component uses `$$props` */
@ -62,7 +62,6 @@ export interface ComponentAnalysis extends Analysis {
/** If `true`, should append styles through JavaScript */ /** If `true`, should append styles through JavaScript */
inject_styles: boolean; inject_styles: boolean;
reactive_statements: Map<LabeledStatement, ReactiveStatement>; reactive_statements: Map<LabeledStatement, ReactiveStatement>;
top_level_snippets: VariableDeclaration[];
/** Identifiers that make up the `bind:group` expression -> internal group binding name */ /** Identifiers that make up the `bind:group` expression -> internal group binding name */
binding_groups: Map<[key: string, bindings: Array<Binding | null>], Identifier>; binding_groups: Map<[key: string, bindings: Array<Binding | null>], Identifier>;
slot_names: Map<string, AST.SlotElement>; slot_names: Map<string, AST.SlotElement>;
@ -72,6 +71,18 @@ export interface ComponentAnalysis extends Analysis {
keyframes: string[]; keyframes: string[];
}; };
source: string; source: string;
undefined_exports: Map<string, Node>;
/**
* Every render tag/component, and whether it could be definitively resolved or not
*/
snippet_renderers: Map<
AST.RenderTag | AST.Component | AST.SvelteComponent | AST.SvelteSelf,
boolean
>;
/**
* Every snippet that is declared locally
*/
snippets: Set<AST.SnippetBlock>;
} }
declare module 'estree' { declare module 'estree' {

@ -279,7 +279,8 @@ export interface Binding {
| 'snippet' | 'snippet'
| 'store_sub' | 'store_sub'
| 'legacy_reactive' | 'legacy_reactive'
| 'template'; | 'template'
| 'snippet';
declaration_kind: DeclarationKind; declaration_kind: DeclarationKind;
/** /**
* What the value was initialized with. * What the value was initialized with.

@ -166,6 +166,9 @@ export namespace AST {
dynamic: boolean; dynamic: boolean;
args_with_call_expression: Set<number>; args_with_call_expression: Set<number>;
path: SvelteNode[]; path: SvelteNode[];
/** The set of locally-defined snippets that this render tag could correspond to,
* used for CSS pruning purposes */
snippets: Set<SnippetBlock>;
}; };
} }
@ -278,6 +281,10 @@ export namespace AST {
metadata: { metadata: {
scopes: Record<string, Scope>; scopes: Record<string, Scope>;
dynamic: boolean; dynamic: boolean;
/** The set of locally-defined snippets that this component tag could render,
* used for CSS pruning purposes */
snippets: Set<SnippetBlock>;
path: SvelteNode[];
}; };
} }
@ -323,6 +330,10 @@ export namespace AST {
/** @internal */ /** @internal */
metadata: { metadata: {
scopes: Record<string, Scope>; scopes: Record<string, Scope>;
/** The set of locally-defined snippets that this component tag could render,
* used for CSS pruning purposes */
snippets: Set<SnippetBlock>;
path: SvelteNode[];
}; };
} }
@ -357,6 +368,11 @@ export namespace AST {
name: 'svelte:fragment'; name: 'svelte:fragment';
} }
export interface SvelteBoundary extends BaseElement {
type: 'SvelteBoundary';
name: 'svelte:boundary';
}
export interface SvelteHead extends BaseElement { export interface SvelteHead extends BaseElement {
type: 'SvelteHead'; type: 'SvelteHead';
name: 'svelte:head'; name: 'svelte:head';
@ -374,6 +390,10 @@ export namespace AST {
/** @internal */ /** @internal */
metadata: { metadata: {
scopes: Record<string, Scope>; scopes: Record<string, Scope>;
/** The set of locally-defined snippets that this component tag could render,
* used for CSS pruning purposes */
snippets: Set<SnippetBlock>;
path: SvelteNode[];
}; };
} }
@ -386,7 +406,8 @@ export namespace AST {
export interface EachBlock extends BaseNode { export interface EachBlock extends BaseNode {
type: 'EachBlock'; type: 'EachBlock';
expression: Expression; expression: Expression;
context: Pattern; /** The `entry` in `{#each item as entry}`. `null` if `as` part is omitted */
context: Pattern | null;
body: Fragment; body: Fragment;
fallback?: Fragment; fallback?: Fragment;
index?: string; index?: string;
@ -443,6 +464,13 @@ export namespace AST {
expression: Identifier; expression: Identifier;
parameters: Pattern[]; parameters: Pattern[];
body: Fragment; body: Fragment;
/** @internal */
metadata: {
can_hoist: boolean;
/** The set of components/render tags that could render this snippet,
* used for CSS pruning */
sites: Set<Component | SvelteComponent | SvelteSelf | RenderTag>;
};
} }
export interface Attribute extends BaseNode { export interface Attribute extends BaseNode {
@ -505,7 +533,8 @@ export type ElementLike =
| AST.SvelteHead | AST.SvelteHead
| AST.SvelteOptionsRaw | AST.SvelteOptionsRaw
| AST.SvelteSelf | AST.SvelteSelf
| AST.SvelteWindow; | AST.SvelteWindow
| AST.SvelteBoundary;
export type TemplateNode = export type TemplateNode =
| AST.Root | AST.Root

@ -102,7 +102,6 @@ export const codes = [
"perf_avoid_nested_class", "perf_avoid_nested_class",
"reactive_declaration_invalid_placement", "reactive_declaration_invalid_placement",
"reactive_declaration_module_script_dependency", "reactive_declaration_module_script_dependency",
"reactive_declaration_non_reactive_property",
"state_referenced_locally", "state_referenced_locally",
"store_rune_conflict", "store_rune_conflict",
"css_unused_selector", "css_unused_selector",
@ -641,14 +640,6 @@ export function reactive_declaration_module_script_dependency(node) {
w(node, "reactive_declaration_module_script_dependency", "Reassignments of module-level declarations will not cause reactive statements to update"); w(node, "reactive_declaration_module_script_dependency", "Reassignments of module-level declarations will not cause reactive statements to update");
} }
/**
* Properties of objects and arrays are not reactive unless in runes mode. Changes to this property will not cause the reactive statement to update
* @param {null | NodeLike} node
*/
export function reactive_declaration_non_reactive_property(node) {
w(node, "reactive_declaration_non_reactive_property", "Properties of objects and arrays are not reactive unless in runes mode. Changes to this property will not cause the reactive statement to update");
}
/** /**
* State referenced in its own scope will never update. Did you mean to reference it inside a closure? * State referenced in its own scope will never update. Did you mean to reference it inside a closure?
* @param {null | NodeLike} node * @param {null | NodeLike} node
@ -763,13 +754,12 @@ export function event_directive_deprecated(node, name) {
} }
/** /**
* %thing% is invalid inside `<%parent%>`. When rendering this component on the server, the resulting HTML will be modified by the browser, likely resulting in a `hydration_mismatch` warning * %message%. When rendering this component on the server, the resulting HTML will be modified by the browser (by moving, removing, or inserting elements), likely resulting in a `hydration_mismatch` warning
* @param {null | NodeLike} node * @param {null | NodeLike} node
* @param {string} thing * @param {string} message
* @param {string} parent
*/ */
export function node_invalid_placement_ssr(node, thing, parent) { export function node_invalid_placement_ssr(node, message) {
w(node, "node_invalid_placement_ssr", `${thing} is invalid inside \`<${parent}>\`. When rendering this component on the server, the resulting HTML will be modified by the browser, likely resulting in a \`hydration_mismatch\` warning`); w(node, "node_invalid_placement_ssr", `${message}. When rendering this component on the server, the resulting HTML will be modified by the browser (by moving, removing, or inserting elements), likely resulting in a \`hydration_mismatch\` warning`);
} }
/** /**

@ -44,7 +44,8 @@ export const IGNORABLE_RUNTIME_WARNINGS = /** @type {const} */ ([
'hydration_attribute_changed', 'hydration_attribute_changed',
'hydration_html_changed', 'hydration_html_changed',
'ownership_invalid_binding', 'ownership_invalid_binding',
'ownership_invalid_mutation' 'ownership_invalid_mutation',
'reactive_declaration_non_reactive_property'
]); ]);
/** /**

@ -135,59 +135,85 @@ const disallowed_children = {
}; };
/** /**
* Returns false if the tag is not allowed inside the ancestor tag (which is grandparent and above) such that it will result * Returns an error message if the tag is not allowed inside the ancestor tag (which is grandparent and above) such that it will result
* in the browser repairing the HTML, which will likely result in an error during hydration. * in the browser repairing the HTML, which will likely result in an error during hydration.
* @param {string} tag * @param {string} child_tag
* @param {string[]} ancestors All nodes starting with the parent, up until the ancestor, which means two entries minimum * @param {string[]} ancestors All nodes starting with the parent, up until the ancestor, which means two entries minimum
* @returns {boolean} * @param {string} [child_loc]
* @param {string} [ancestor_loc]
* @returns {string | null}
*/ */
export function is_tag_valid_with_ancestor(tag, ancestors) { export function is_tag_valid_with_ancestor(child_tag, ancestors, child_loc, ancestor_loc) {
if (tag.includes('-')) return true; // custom elements can be anything if (child_tag.includes('-')) return null; // custom elements can be anything
const target = ancestors[ancestors.length - 1]; const ancestor_tag = ancestors[ancestors.length - 1];
const disallowed = disallowed_children[target]; const disallowed = disallowed_children[ancestor_tag];
if (!disallowed) return true; if (!disallowed) return null;
if ('reset_by' in disallowed && disallowed.reset_by) { if ('reset_by' in disallowed && disallowed.reset_by) {
for (let i = ancestors.length - 2; i >= 0; i--) { for (let i = ancestors.length - 2; i >= 0; i--) {
const ancestor = ancestors[i]; const ancestor = ancestors[i];
if (ancestor.includes('-')) return true; // custom elements can be anything if (ancestor.includes('-')) return null; // custom elements can be anything
// A reset means that forbidden descendants are allowed again // A reset means that forbidden descendants are allowed again
if (disallowed.reset_by.includes(ancestors[i])) { if (disallowed.reset_by.includes(ancestors[i])) {
return true; return null;
}
} }
} }
if ('descendant' in disallowed && disallowed.descendant.includes(child_tag)) {
const child = child_loc ? `\`<${child_tag}>\` (${child_loc})` : `\`<${child_tag}>\``;
const ancestor = ancestor_loc
? `\`<${ancestor_tag}>\` (${ancestor_loc})`
: `\`<${ancestor_tag}>\``;
return `${child} cannot be a descendant of ${ancestor}`;
} }
return 'descendant' in disallowed ? !disallowed.descendant.includes(tag) : true; return null;
} }
/** /**
* Returns false if the tag is not allowed inside the parent tag such that it will result * Returns an error message if the tag is not allowed inside the parent tag such that it will result
* in the browser repairing the HTML, which will likely result in an error during hydration. * in the browser repairing the HTML, which will likely result in an error during hydration.
* @param {string} tag * @param {string} child_tag
* @param {string} parent_tag * @param {string} parent_tag
* @returns {boolean} * @param {string} [child_loc]
* @param {string} [parent_loc]
* @returns {string | null}
*/ */
export function is_tag_valid_with_parent(tag, parent_tag) { export function is_tag_valid_with_parent(child_tag, parent_tag, child_loc, parent_loc) {
if (tag.includes('-') || parent_tag?.includes('-')) return true; // custom elements can be anything if (child_tag.includes('-') || parent_tag?.includes('-')) return null; // custom elements can be anything
const disallowed = disallowed_children[parent_tag]; const disallowed = disallowed_children[parent_tag];
const child = child_loc ? `\`<${child_tag}>\` (${child_loc})` : `\`<${child_tag}>\``;
const parent = parent_loc ? `\`<${parent_tag}>\` (${parent_loc})` : `\`<${parent_tag}>\``;
if (disallowed) { if (disallowed) {
if ('direct' in disallowed && disallowed.direct.includes(tag)) { if ('direct' in disallowed && disallowed.direct.includes(child_tag)) {
return false; return `${child} cannot be a direct child of ${parent}`;
} }
if ('descendant' in disallowed && disallowed.descendant.includes(tag)) {
return false; if ('descendant' in disallowed && disallowed.descendant.includes(child_tag)) {
return `${child} cannot be a child of ${parent}`;
} }
if ('only' in disallowed && disallowed.only) { if ('only' in disallowed && disallowed.only) {
return disallowed.only.includes(tag); if (disallowed.only.includes(child_tag)) {
return null;
} else {
return `${child} cannot be a child of ${parent}. \`<${parent_tag}>\` only allows these children: ${disallowed.only.map((d) => `\`<${d}>\``).join(', ')}`;
}
} }
} }
switch (tag) { // These tags are only valid with a few parents that have special child
// parsing rules - if we're down here, then none of those matched and
// so we allow it only if we don't know what the parent is, as all other
// cases are invalid (and we only get into this function if we know the parent).
switch (child_tag) {
case 'body': case 'body':
case 'caption': case 'caption':
case 'col': case 'col':
@ -196,18 +222,17 @@ export function is_tag_valid_with_parent(tag, parent_tag) {
case 'frame': case 'frame':
case 'head': case 'head':
case 'html': case 'html':
return `${child} cannot be a child of ${parent}`;
case 'thead':
case 'tbody': case 'tbody':
case 'td':
case 'tfoot': case 'tfoot':
return `${child} must be the child of a \`<table>\`, not a ${parent}`;
case 'td':
case 'th': case 'th':
case 'thead': return `${child} must be the child of a \`<tr>\`, not a ${parent}`;
case 'tr': case 'tr':
// These tags are only valid with a few parents that have special child return `\`<tr>\` must be the child of a \`<thead>\`, \`<tbody>\`, or \`<tfoot>\`, not a ${parent}`;
// parsing rules - if we're down here, then none of those matched and
// so we allow it only if we don't know what the parent is, as all other
// cases are invalid (and we only get into this function if we know the parent).
return false;
} }
return true; return null;
} }

@ -83,7 +83,7 @@ function create_custom_event(type, detail, { bubbles = false, cancelable = false
* }>(); * }>();
* ``` * ```
* *
* @deprecated Use callback props and/or the `$host()` rune instead see https://svelte.dev/docs/svelte/v5-migration-guide#Event-changes-Component-events * @deprecated Use callback props and/or the `$host()` rune instead see [migration guide](https://svelte.dev/docs/svelte/v5-migration-guide#Event-changes-Component-events)
* @template {Record<string, any>} [EventMap = any] * @template {Record<string, any>} [EventMap = any]
* @returns {EventDispatcher<EventMap>} * @returns {EventDispatcher<EventMap>}
*/ */
@ -122,7 +122,7 @@ export function createEventDispatcher() {
* *
* In runes mode use `$effect.pre` instead. * In runes mode use `$effect.pre` instead.
* *
* @deprecated Use `$effect.pre` instead see https://svelte.dev/docs/svelte/$effect#$effect.pre * @deprecated Use [`$effect.pre`](https://svelte.dev/docs/svelte/$effect#$effect.pre) instead
* @param {() => void} fn * @param {() => void} fn
* @returns {void} * @returns {void}
*/ */
@ -145,7 +145,7 @@ export function beforeUpdate(fn) {
* *
* In runes mode use `$effect` instead. * In runes mode use `$effect` instead.
* *
* @deprecated Use `$effect` instead see https://svelte.dev/docs/svelte/$effect * @deprecated Use [`$effect`](https://svelte.dev/docs/svelte/$effect) instead
* @param {() => void} fn * @param {() => void} fn
* @returns {void} * @returns {void}
*/ */

@ -38,7 +38,7 @@ type Properties<Props, Slots> = Props &
/** /**
* This was the base class for Svelte components in Svelte 4. Svelte 5+ components * This was the base class for Svelte components in Svelte 4. Svelte 5+ components
* are completely different under the hood. For typing, use `Component` instead. * are completely different under the hood. For typing, use `Component` instead.
* To instantiate components, use `mount` instead`. * To instantiate components, use `mount` instead.
* See [migration guide](https://svelte.dev/docs/svelte/v5-migration-guide#Components-are-no-longer-classes) for more info. * See [migration guide](https://svelte.dev/docs/svelte/v5-migration-guide#Components-are-no-longer-classes) for more info.
*/ */
export class SvelteComponent< export class SvelteComponent<
@ -53,7 +53,7 @@ export class SvelteComponent<
/** /**
* @deprecated This constructor only exists when using the `asClassComponent` compatibility helper, which * @deprecated This constructor only exists when using the `asClassComponent` compatibility helper, which
* is a stop-gap solution. Migrate towards using `mount` instead. See * is a stop-gap solution. Migrate towards using `mount` instead. See
* https://svelte.dev/docs/svelte/v5-migration-guide#Components-are-no-longer-classes for more info. * [migration guide](https://svelte.dev/docs/svelte/v5-migration-guide#Components-are-no-longer-classes) for more info.
*/ */
constructor(options: ComponentConstructorOptions<Properties<Props, Slots>>); constructor(options: ComponentConstructorOptions<Properties<Props, Slots>>);
/** /**
@ -83,14 +83,14 @@ export class SvelteComponent<
/** /**
* @deprecated This method only exists when using one of the legacy compatibility helpers, which * @deprecated This method only exists when using one of the legacy compatibility helpers, which
* is a stop-gap solution. See https://svelte.dev/docs/svelte/v5-migration-guide#Components-are-no-longer-classes * is a stop-gap solution. See [migration guide](https://svelte.dev/docs/svelte/v5-migration-guide#Components-are-no-longer-classes)
* for more info. * for more info.
*/ */
$destroy(): void; $destroy(): void;
/** /**
* @deprecated This method only exists when using one of the legacy compatibility helpers, which * @deprecated This method only exists when using one of the legacy compatibility helpers, which
* is a stop-gap solution. See https://svelte.dev/docs/svelte/v5-migration-guide#Components-are-no-longer-classes * is a stop-gap solution. See [migration guide](https://svelte.dev/docs/svelte/v5-migration-guide#Components-are-no-longer-classes)
* for more info. * for more info.
*/ */
$on<K extends Extract<keyof Events, string>>( $on<K extends Extract<keyof Events, string>>(
@ -100,7 +100,7 @@ export class SvelteComponent<
/** /**
* @deprecated This method only exists when using one of the legacy compatibility helpers, which * @deprecated This method only exists when using one of the legacy compatibility helpers, which
* is a stop-gap solution. See https://svelte.dev/docs/svelte/v5-migration-guide#Components-are-no-longer-classes * is a stop-gap solution. See [migration guide](https://svelte.dev/docs/svelte/v5-migration-guide#Components-are-no-longer-classes)
* for more info. * for more info.
*/ */
$set(props: Partial<Props>): void; $set(props: Partial<Props>): void;
@ -153,13 +153,13 @@ export interface Component<
): { ): {
/** /**
* @deprecated This method only exists when using one of the legacy compatibility helpers, which * @deprecated This method only exists when using one of the legacy compatibility helpers, which
* is a stop-gap solution. See https://svelte.dev/docs/svelte/v5-migration-guide#Components-are-no-longer-classes * is a stop-gap solution. See [migration guide](https://svelte.dev/docs/svelte/v5-migration-guide#Components-are-no-longer-classes)
* for more info. * for more info.
*/ */
$on?(type: string, callback: (e: any) => void): () => void; $on?(type: string, callback: (e: any) => void): () => void;
/** /**
* @deprecated This method only exists when using one of the legacy compatibility helpers, which * @deprecated This method only exists when using one of the legacy compatibility helpers, which
* is a stop-gap solution. See https://svelte.dev/docs/svelte/v5-migration-guide#Components-are-no-longer-classes * is a stop-gap solution. See [migration guide](https://svelte.dev/docs/svelte/v5-migration-guide#Components-are-no-longer-classes)
* for more info. * for more info.
*/ */
$set?(props: Partial<Props>): void; $set?(props: Partial<Props>): void;

@ -4,21 +4,22 @@ export const RENDER_EFFECT = 1 << 3;
export const BLOCK_EFFECT = 1 << 4; export const BLOCK_EFFECT = 1 << 4;
export const BRANCH_EFFECT = 1 << 5; export const BRANCH_EFFECT = 1 << 5;
export const ROOT_EFFECT = 1 << 6; export const ROOT_EFFECT = 1 << 6;
export const UNOWNED = 1 << 7; export const BOUNDARY_EFFECT = 1 << 7;
export const DISCONNECTED = 1 << 8; export const UNOWNED = 1 << 8;
export const CLEAN = 1 << 9; export const DISCONNECTED = 1 << 9;
export const DIRTY = 1 << 10; export const CLEAN = 1 << 10;
export const MAYBE_DIRTY = 1 << 11; export const DIRTY = 1 << 11;
export const INERT = 1 << 12; export const MAYBE_DIRTY = 1 << 12;
export const DESTROYED = 1 << 13; export const INERT = 1 << 13;
export const EFFECT_RAN = 1 << 14; export const DESTROYED = 1 << 14;
export const EFFECT_RAN = 1 << 15;
/** 'Transparent' effects do not create a transition boundary */ /** 'Transparent' effects do not create a transition boundary */
export const EFFECT_TRANSPARENT = 1 << 15; export const EFFECT_TRANSPARENT = 1 << 16;
/** Svelte 4 legacy mode props need to be handled with deriveds and be recognized elsewhere, hence the dedicated flag */ /** Svelte 4 legacy mode props need to be handled with deriveds and be recognized elsewhere, hence the dedicated flag */
export const LEGACY_DERIVED_PROP = 1 << 16; export const LEGACY_DERIVED_PROP = 1 << 17;
export const INSPECT_EFFECT = 1 << 17; export const INSPECT_EFFECT = 1 << 18;
export const HEAD_EFFECT = 1 << 18; export const HEAD_EFFECT = 1 << 19;
export const EFFECT_HAS_DERIVED = 1 << 19; export const EFFECT_HAS_DERIVED = 1 << 20;
export const STATE_SYMBOL = Symbol('$state'); export const STATE_SYMBOL = Symbol('$state');
export const STATE_SYMBOL_METADATA = Symbol('$state metadata'); export const STATE_SYMBOL_METADATA = Symbol('$state metadata');

@ -0,0 +1,57 @@
import * as w from '../warnings.js';
import { sanitize_location } from './location.js';
/**
*
* @param {any} a
* @param {any} b
* @param {string} property
* @param {string} location
*/
function compare(a, b, property, location) {
if (a !== b) {
w.assignment_value_stale(property, /** @type {string} */ (sanitize_location(location)));
}
return a;
}
/**
* @param {any} object
* @param {string} property
* @param {any} value
* @param {string} location
*/
export function assign(object, property, value, location) {
return compare((object[property] = value), object[property], property, location);
}
/**
* @param {any} object
* @param {string} property
* @param {any} value
* @param {string} location
*/
export function assign_and(object, property, value, location) {
return compare((object[property] &&= value), object[property], property, location);
}
/**
* @param {any} object
* @param {string} property
* @param {any} value
* @param {string} location
*/
export function assign_or(object, property, value, location) {
return compare((object[property] ||= value), object[property], property, location);
}
/**
* @param {any} object
* @param {string} property
* @param {any} value
* @param {string} location
*/
export function assign_nullish(object, property, value, location) {
return compare((object[property] ??= value), object[property], property, location);
}

@ -0,0 +1,25 @@
import { DEV } from 'esm-env';
import { FILENAME } from '../../../constants.js';
import { dev_current_component_function } from '../runtime.js';
/**
*
* @param {number} [line]
* @param {number} [column]
*/
export function get_location(line, column) {
if (!DEV || line === undefined) return undefined;
var filename = dev_current_component_function?.[FILENAME];
var location = filename && `${filename}:${line}:${column}`;
return sanitize_location(location);
}
/**
* Prevent devtools trying to make `location` a clickable link by inserting a zero-width space
* @param {string | undefined} location
*/
export function sanitize_location(location) {
return location?.replace(/\//g, '/\u200b');
}

@ -0,0 +1,134 @@
/** @import { Effect, TemplateNode, } from '#client' */
import { BOUNDARY_EFFECT, EFFECT_TRANSPARENT } from '../../constants.js';
import { block, branch, destroy_effect, pause_effect } from '../../reactivity/effects.js';
import {
active_effect,
active_reaction,
component_context,
handle_error,
set_active_effect,
set_active_reaction,
set_component_context,
reset_is_throwing_error
} from '../../runtime.js';
import {
hydrate_next,
hydrate_node,
hydrating,
next,
remove_nodes,
set_hydrate_node
} from '../hydration.js';
import { queue_micro_task } from '../task.js';
/**
* @param {Effect} boundary
* @param {() => void} fn
*/
function with_boundary(boundary, fn) {
var previous_effect = active_effect;
var previous_reaction = active_reaction;
var previous_ctx = component_context;
set_active_effect(boundary);
set_active_reaction(boundary);
set_component_context(boundary.ctx);
try {
fn();
} finally {
set_active_effect(previous_effect);
set_active_reaction(previous_reaction);
set_component_context(previous_ctx);
}
}
/**
* @param {TemplateNode} node
* @param {{
* onerror?: (error: unknown, reset: () => void) => void,
* failed?: (anchor: Node, error: () => unknown, reset: () => () => void) => void
* }} props
* @param {((anchor: Node) => void)} boundary_fn
* @returns {void}
*/
export function boundary(node, props, boundary_fn) {
var anchor = node;
/** @type {Effect} */
var boundary_effect;
block(() => {
var boundary = /** @type {Effect} */ (active_effect);
var hydrate_open = hydrate_node;
var is_creating_fallback = false;
// We re-use the effect's fn property to avoid allocation of an additional field
boundary.fn = (/** @type {unknown}} */ error) => {
var onerror = props.onerror;
let failed = props.failed;
// If we have nothing to capture the error, or if we hit an error while
// rendering the fallback, re-throw for another boundary to handle
if ((!onerror && !failed) || is_creating_fallback) {
throw error;
}
var reset = () => {
pause_effect(boundary_effect);
with_boundary(boundary, () => {
is_creating_fallback = false;
boundary_effect = branch(() => boundary_fn(anchor));
reset_is_throwing_error();
});
};
onerror?.(error, reset);
if (boundary_effect) {
destroy_effect(boundary_effect);
} else if (hydrating) {
set_hydrate_node(hydrate_open);
next();
set_hydrate_node(remove_nodes());
}
if (failed) {
// Render the `failed` snippet in a microtask
queue_micro_task(() => {
with_boundary(boundary, () => {
is_creating_fallback = true;
try {
boundary_effect = branch(() => {
failed(
anchor,
() => error,
() => reset
);
});
} catch (error) {
handle_error(error, boundary, null, boundary.ctx);
}
reset_is_throwing_error();
is_creating_fallback = false;
});
});
}
};
if (hydrating) {
hydrate_next();
}
boundary_effect = branch(() => boundary_fn(anchor));
reset_is_throwing_error();
}, EFFECT_TRANSPARENT | BOUNDARY_EFFECT);
if (hydrating) {
anchor = hydrate_node;
}
}

@ -9,6 +9,7 @@ import { hash } from '../../../../utils.js';
import { DEV } from 'esm-env'; import { DEV } from 'esm-env';
import { dev_current_component_function } from '../../runtime.js'; import { dev_current_component_function } from '../../runtime.js';
import { get_first_child, get_next_sibling } from '../operations.js'; import { get_first_child, get_next_sibling } from '../operations.js';
import { sanitize_location } from '../../dev/location.js';
/** /**
* @param {Element} element * @param {Element} element
@ -28,9 +29,7 @@ function check_hash(element, server_hash, value) {
location = `in ${dev_current_component_function[FILENAME]}`; location = `in ${dev_current_component_function[FILENAME]}`;
} }
w.hydration_html_changed( w.hydration_html_changed(sanitize_location(location));
location?.replace(/\//g, '/\u200b') // prevent devtools trying to make it a clickable link by inserting a zero-width space
);
} }
/** /**

@ -13,13 +13,11 @@ import { HYDRATION_START_ELSE } from '../../../../constants.js';
/** /**
* @param {TemplateNode} node * @param {TemplateNode} node
* @param {() => boolean} get_condition * @param {(branch: (fn: (anchor: Node) => void, flag?: boolean) => void) => void} fn
* @param {(anchor: Node) => void} consequent_fn
* @param {null | ((anchor: Node) => void)} [alternate_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 {boolean} [elseif] True if this is an `{:else if ...}` block rather than an `{#if ...}`, as that affects which transitions are considered 'local'
* @returns {void} * @returns {void}
*/ */
export function if_block(node, get_condition, consequent_fn, alternate_fn = null, elseif = false) { export function if_block(node, fn, elseif = false) {
if (hydrating) { if (hydrating) {
hydrate_next(); hydrate_next();
} }
@ -37,8 +35,18 @@ export function if_block(node, get_condition, consequent_fn, alternate_fn = null
var flags = elseif ? EFFECT_TRANSPARENT : 0; var flags = elseif ? EFFECT_TRANSPARENT : 0;
block(() => { var has_branch = false;
if (condition === (condition = !!get_condition())) return;
const set_branch = (/** @type {(anchor: Node) => 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
) => {
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 */ /** Whether or not there was a hydration mismatch. Needs to be a `let` or else it isn't treeshaken out */
let mismatch = false; let mismatch = false;
@ -60,8 +68,8 @@ export function if_block(node, get_condition, consequent_fn, alternate_fn = null
if (condition) { if (condition) {
if (consequent_effect) { if (consequent_effect) {
resume_effect(consequent_effect); resume_effect(consequent_effect);
} else { } else if (fn) {
consequent_effect = branch(() => consequent_fn(anchor)); consequent_effect = branch(() => fn(anchor));
} }
if (alternate_effect) { if (alternate_effect) {
@ -72,8 +80,8 @@ export function if_block(node, get_condition, consequent_fn, alternate_fn = null
} else { } else {
if (alternate_effect) { if (alternate_effect) {
resume_effect(alternate_effect); resume_effect(alternate_effect);
} else if (alternate_fn) { } else if (fn) {
alternate_effect = branch(() => alternate_fn(anchor)); alternate_effect = branch(() => fn(anchor));
} }
if (consequent_effect) { if (consequent_effect) {
@ -87,6 +95,14 @@ export function if_block(node, get_condition, consequent_fn, alternate_fn = null
// continue in hydration mode // continue in hydration mode
set_hydrating(true); set_hydrating(true);
} }
};
block(() => {
has_branch = false;
fn(set_branch);
if (!has_branch) {
update_branch(null, null);
}
}, flags); }, flags);
if (hydrating) { if (hydrating) {

@ -60,13 +60,19 @@ export function remove_input_defaults(input) {
export function set_value(element, value) { export function set_value(element, value) {
// @ts-expect-error // @ts-expect-error
var attributes = (element.__attributes ??= {}); var attributes = (element.__attributes ??= {});
if ( if (
attributes.value === (attributes.value = value) || attributes.value ===
(attributes.value =
// treat null and undefined the same for the initial value
value ?? undefined) ||
// @ts-expect-error // @ts-expect-error
// `progress` elements always need their value set when its `0` // `progress` elements always need their value set when its `0`
(element.value === value && (value !== 0 || element.nodeName !== 'PROGRESS')) (element.value === value && (value !== 0 || element.nodeName !== 'PROGRESS'))
) ) {
return; return;
}
// @ts-expect-error // @ts-expect-error
element.value = value; element.value = value;
} }
@ -79,11 +85,60 @@ export function set_checked(element, checked) {
// @ts-expect-error // @ts-expect-error
var attributes = (element.__attributes ??= {}); var attributes = (element.__attributes ??= {});
if (attributes.checked === (attributes.checked = checked)) return; if (
attributes.checked ===
(attributes.checked =
// treat null and undefined the same for the initial value
checked ?? undefined)
) {
return;
}
// @ts-expect-error // @ts-expect-error
element.checked = checked; element.checked = checked;
} }
/**
* Sets the `selected` attribute on an `option` element.
* Not set through the property because that doesn't reflect to the DOM,
* which means it wouldn't be taken into account when a form is reset.
* @param {HTMLOptionElement} element
* @param {boolean} selected
*/
export function set_selected(element, selected) {
if (selected) {
// The selected option could've changed via user selection, and
// setting the value without this check would set it back.
if (!element.hasAttribute('selected')) {
element.setAttribute('selected', '');
}
} else {
element.removeAttribute('selected');
}
}
/**
* Applies the default checked property without influencing the current checked property.
* @param {HTMLInputElement} element
* @param {boolean} checked
*/
export function set_default_checked(element, checked) {
const existing_value = element.checked;
element.defaultChecked = checked;
element.checked = existing_value;
}
/**
* Applies the default value property without influencing the current value property.
* @param {HTMLInputElement | HTMLTextAreaElement} element
* @param {string} value
*/
export function set_default_value(element, value) {
const existing_value = element.value;
element.defaultValue = value;
element.value = existing_value;
}
/** /**
* @param {Element} element * @param {Element} element
* @param {string} attribute * @param {string} attribute
@ -281,6 +336,9 @@ export function set_attributes(
element[`__${event_name}`] = value; element[`__${event_name}`] = value;
delegate([event_name]); delegate([event_name]);
} }
} else if (delegated) {
// @ts-ignore
element[`__${event_name}`] = undefined;
} }
} else if (key === 'style' && value != null) { } else if (key === 'style' && value != null) {
element.style.cssText = value + ''; element.style.cssText = value + '';

@ -1,11 +1,11 @@
import { DEV } from 'esm-env'; import { DEV } from 'esm-env';
import { render_effect, teardown } from '../../../reactivity/effects.js'; import { render_effect, teardown } from '../../../reactivity/effects.js';
import { listen_to_event_and_reset_event, without_reactive_context } from './shared.js'; import { listen_to_event_and_reset_event } from './shared.js';
import * as e from '../../../errors.js'; import * as e from '../../../errors.js';
import { is } from '../../../proxy.js'; import { is } from '../../../proxy.js';
import { queue_micro_task } from '../../task.js'; import { queue_micro_task } from '../../task.js';
import { hydrating } from '../../hydration.js'; import { hydrating } from '../../hydration.js';
import { is_runes } from '../../../runtime.js'; import { is_runes, untrack } from '../../../runtime.js';
/** /**
* @param {HTMLInputElement} input * @param {HTMLInputElement} input
@ -16,24 +16,36 @@ import { is_runes } from '../../../runtime.js';
export function bind_value(input, get, set = get) { export function bind_value(input, get, set = get) {
var runes = is_runes(); var runes = is_runes();
listen_to_event_and_reset_event(input, 'input', () => { listen_to_event_and_reset_event(input, 'input', (is_reset) => {
if (DEV && input.type === 'checkbox') { if (DEV && input.type === 'checkbox') {
// TODO should this happen in prod too? // TODO should this happen in prod too?
e.bind_invalid_checkbox_value(); e.bind_invalid_checkbox_value();
} }
/** @type {unknown} */ /** @type {any} */
var value = is_numberlike_input(input) ? to_number(input.value) : input.value; var value = is_reset ? input.defaultValue : input.value;
value = is_numberlike_input(input) ? to_number(value) : value;
set(value); set(value);
// In runes mode, respect any validation in accessors (doesn't apply in legacy mode, // In runes mode, respect any validation in accessors (doesn't apply in legacy mode,
// because we use mutable state which ensures the render effect always runs) // because we use mutable state which ensures the render effect always runs)
if (runes && value !== (value = get())) { if (runes && value !== (value = get())) {
// @ts-expect-error the value is coerced on assignment // the value is coerced on assignment
input.value = value ?? ''; input.value = value ?? '';
} }
}); });
if (
// If we are hydrating and the value has since changed,
// then use the updated value from the input instead.
(hydrating && input.defaultValue !== input.value) ||
// If defaultValue is set, then value == defaultValue
// TODO Svelte 6: remove input.value check and set to empty string?
(untrack(get) == null && input.value)
) {
set(is_numberlike_input(input) ? to_number(input.value) : input.value);
}
render_effect(() => { render_effect(() => {
if (DEV && input.type === 'checkbox') { if (DEV && input.type === 'checkbox') {
// TODO should this happen in prod too? // TODO should this happen in prod too?
@ -42,13 +54,6 @@ export function bind_value(input, get, set = get) {
var value = get(); var value = get();
// If we are hydrating and the value has since changed, then use the update value
// from the input instead.
if (hydrating && input.defaultValue !== input.value) {
set(is_numberlike_input(input) ? to_number(input.value) : input.value);
return;
}
if (is_numberlike_input(input) && value === to_number(input.value)) { if (is_numberlike_input(input) && value === to_number(input.value)) {
// handles 0 vs 00 case (see https://github.com/sveltejs/svelte/issues/9959) // handles 0 vs 00 case (see https://github.com/sveltejs/svelte/issues/9959)
return; return;
@ -175,13 +180,19 @@ export function bind_group(inputs, group_index, input, get, set = get) {
* @returns {void} * @returns {void}
*/ */
export function bind_checked(input, get, set = get) { export function bind_checked(input, get, set = get) {
listen_to_event_and_reset_event(input, 'change', () => { listen_to_event_and_reset_event(input, 'change', (is_reset) => {
var value = input.checked; var value = is_reset ? input.defaultChecked : input.checked;
set(value); set(value);
}); });
if (get() == undefined) { if (
set(false); // If we are hydrating and the value has since changed,
// then use the update value from the input instead.
(hydrating && input.defaultChecked !== input.checked) ||
// If defaultChecked is set, then checked == defaultChecked
untrack(get) == null
) {
set(input.checked);
} }
render_effect(() => { render_effect(() => {

@ -80,15 +80,19 @@ export function init_select(select, get_value) {
export function bind_select_value(select, get, set = get) { export function bind_select_value(select, get, set = get) {
var mounting = true; var mounting = true;
listen_to_event_and_reset_event(select, 'change', () => { listen_to_event_and_reset_event(select, 'change', (is_reset) => {
var query = is_reset ? '[selected]' : ':checked';
/** @type {unknown} */ /** @type {unknown} */
var value; var value;
if (select.multiple) { if (select.multiple) {
value = [].map.call(select.querySelectorAll(':checked'), get_option_value); value = [].map.call(select.querySelectorAll(query), get_option_value);
} else { } else {
/** @type {HTMLOptionElement | null} */ /** @type {HTMLOptionElement | null} */
var selected_option = select.querySelector(':checked'); var selected_option =
select.querySelector(query) ??
// will fall back to first non-disabled option if no option is selected
select.querySelector('option:not([disabled])');
value = selected_option && get_option_value(selected_option); value = selected_option && get_option_value(selected_option);
} }

@ -53,8 +53,8 @@ export function without_reactive_context(fn) {
* to notify all bindings when the form is reset * to notify all bindings when the form is reset
* @param {HTMLElement} element * @param {HTMLElement} element
* @param {string} event * @param {string} event
* @param {() => void} handler * @param {(is_reset?: true) => void} handler
* @param {() => void} [on_reset] * @param {(is_reset?: true) => void} [on_reset]
*/ */
export function listen_to_event_and_reset_event(element, event, handler, on_reset = handler) { export function listen_to_event_and_reset_event(element, event, handler, on_reset = handler) {
element.addEventListener(event, () => without_reactive_context(handler)); element.addEventListener(event, () => without_reactive_context(handler));
@ -65,11 +65,11 @@ export function listen_to_event_and_reset_event(element, event, handler, on_rese
// @ts-expect-error // @ts-expect-error
element.__on_r = () => { element.__on_r = () => {
prev(); prev();
on_reset(); on_reset(true);
}; };
} else { } else {
// @ts-expect-error // @ts-expect-error
element.__on_r = on_reset; element.__on_r = () => on_reset(true);
} }
add_form_reset_listener(); add_form_reset_listener();

@ -4,7 +4,6 @@ import { create_text, get_first_child } from './operations.js';
import { create_fragment_from_html } from './reconciler.js'; import { create_fragment_from_html } from './reconciler.js';
import { active_effect } from '../runtime.js'; import { active_effect } from '../runtime.js';
import { TEMPLATE_FRAGMENT, TEMPLATE_USE_IMPORT_NODE } from '../../../constants.js'; import { TEMPLATE_FRAGMENT, TEMPLATE_USE_IMPORT_NODE } from '../../../constants.js';
import { queue_micro_task } from './task.js';
/** /**
* @param {TemplateNode} start * @param {TemplateNode} start

@ -1,4 +1,5 @@
export { FILENAME, HMR, NAMESPACE_SVG } from '../../constants.js'; export { FILENAME, HMR, NAMESPACE_SVG } from '../../constants.js';
export { assign, assign_and, assign_or, assign_nullish } from './dev/assign.js';
export { cleanup_styles } from './dev/css.js'; export { cleanup_styles } from './dev/css.js';
export { add_locations } from './dev/elements.js'; export { add_locations } from './dev/elements.js';
export { hmr } from './dev/hmr.js'; export { hmr } from './dev/hmr.js';
@ -33,7 +34,10 @@ export {
set_xlink_attribute, set_xlink_attribute,
handle_lazy_img, handle_lazy_img,
set_value, set_value,
set_checked set_checked,
set_selected,
set_default_checked,
set_default_value
} from './dom/elements/attributes.js'; } from './dom/elements/attributes.js';
export { set_class, set_svg_class, set_mathml_class, toggle_class } from './dom/elements/class.js'; export { set_class, set_svg_class, set_mathml_class, toggle_class } from './dom/elements/class.js';
export { apply, event, delegate, replay_events } from './dom/elements/events.js'; export { apply, event, delegate, replay_events } from './dom/elements/events.js';
@ -124,6 +128,7 @@ export {
update_store, update_store,
mark_store_binding mark_store_binding
} from './reactivity/store.js'; } from './reactivity/store.js';
export { boundary } from './dom/blocks/boundary.js';
export { set_text } from './render.js'; export { set_text } from './render.js';
export { export {
get, get,

@ -4,10 +4,13 @@ import { raf } from './timing.js';
// TODO move this into timing.js where it probably belongs // TODO move this into timing.js where it probably belongs
/** /**
* @param {number} now
* @returns {void} * @returns {void}
*/ */
function run_tasks(now) { function run_tasks() {
// use `raf.now()` instead of the `requestAnimationFrame` callback argument, because
// otherwise things can get wonky https://github.com/sveltejs/svelte/pull/14541
const now = raf.now();
raf.tasks.forEach((task) => { raf.tasks.forEach((task) => {
if (!task.c(now)) { if (!task.c(now)) {
raf.tasks.delete(task); raf.tasks.delete(task);

@ -16,7 +16,8 @@ import {
set_is_flushing_effect, set_is_flushing_effect,
set_signal_status, set_signal_status,
untrack, untrack,
skip_reaction skip_reaction,
capture_signals
} from '../runtime.js'; } from '../runtime.js';
import { import {
DIRTY, DIRTY,
@ -39,10 +40,13 @@ import {
} from '../constants.js'; } from '../constants.js';
import { set } from './sources.js'; import { set } from './sources.js';
import * as e from '../errors.js'; import * as e from '../errors.js';
import * as w from '../warnings.js';
import { DEV } from 'esm-env'; import { DEV } from 'esm-env';
import { define_property } from '../../shared/utils.js'; import { define_property } from '../../shared/utils.js';
import { get_next_sibling } from '../dom/operations.js'; import { get_next_sibling } from '../dom/operations.js';
import { destroy_derived } from './deriveds.js'; import { destroy_derived } from './deriveds.js';
import { FILENAME } from '../../../constants.js';
import { get_location } from '../dev/location.js';
/** /**
* @param {'$effect' | '$effect.pre' | '$inspect'} rune * @param {'$effect' | '$effect.pre' | '$inspect'} rune
@ -261,14 +265,21 @@ export function effect(fn) {
* Internal representation of `$: ..` * Internal representation of `$: ..`
* @param {() => any} deps * @param {() => any} deps
* @param {() => void | (() => void)} fn * @param {() => void | (() => void)} fn
* @param {number} [line]
* @param {number} [column]
*/ */
export function legacy_pre_effect(deps, fn) { export function legacy_pre_effect(deps, fn, line, column) {
var context = /** @type {ComponentContextLegacy} */ (component_context); var context = /** @type {ComponentContextLegacy} */ (component_context);
/** @type {{ effect: null | Effect, ran: boolean }} */ /** @type {{ effect: null | Effect, ran: boolean }} */
var token = { effect: null, ran: false }; var token = { effect: null, ran: false };
context.l.r1.push(token); context.l.r1.push(token);
if (DEV && line !== undefined) {
var location = get_location(line, column);
var explicit_deps = capture_signals(deps);
}
token.effect = render_effect(() => { token.effect = render_effect(() => {
deps(); deps();
@ -278,7 +289,18 @@ export function legacy_pre_effect(deps, fn) {
token.ran = true; token.ran = true;
set(context.l.r2, true); set(context.l.r2, true);
if (DEV && location) {
var implicit_deps = capture_signals(() => untrack(fn));
for (var signal of implicit_deps) {
if (!explicit_deps.has(signal)) {
w.reactive_declaration_non_reactive_property(/** @type {string} */ (location));
}
}
} else {
untrack(fn); untrack(fn);
}
}); });
} }
@ -464,12 +486,12 @@ export function destroy_effect(effect, remove_dom = true) {
} }
// `first` and `child` are nulled out in destroy_effect_children // `first` and `child` are nulled out in destroy_effect_children
// we don't null out `parent` so that error propagation can work correctly
effect.next = effect.next =
effect.prev = effect.prev =
effect.teardown = effect.teardown =
effect.ctx = effect.ctx =
effect.deps = effect.deps =
effect.parent =
effect.fn = effect.fn =
effect.nodes_start = effect.nodes_start =
effect.nodes_end = effect.nodes_end =

@ -24,7 +24,8 @@ import {
BLOCK_EFFECT, BLOCK_EFFECT,
ROOT_EFFECT, ROOT_EFFECT,
LEGACY_DERIVED_PROP, LEGACY_DERIVED_PROP,
DISCONNECTED DISCONNECTED,
BOUNDARY_EFFECT
} from './constants.js'; } from './constants.js';
import { flush_tasks } from './dom/task.js'; import { flush_tasks } from './dom/task.js';
import { add_owner } from './dev/ownership.js'; import { add_owner } from './dev/ownership.js';
@ -37,15 +38,19 @@ import { legacy_mode_flag } from '../flags/index.js';
const FLUSH_MICROTASK = 0; const FLUSH_MICROTASK = 0;
const FLUSH_SYNC = 1; const FLUSH_SYNC = 1;
// Used for DEV time error handling // Used for DEV time error handling
/** @param {WeakSet<Error>} value */ /** @param {WeakSet<Error>} value */
const handled_errors = new WeakSet(); const handled_errors = new WeakSet();
export let is_throwing_error = false;
// Used for controlling the flush of effects. // Used for controlling the flush of effects.
let scheduler_mode = FLUSH_MICROTASK; let scheduler_mode = FLUSH_MICROTASK;
// Used for handling scheduling // Used for handling scheduling
let is_micro_task_queued = false; let is_micro_task_queued = false;
/** @type {Effect | null} */
let last_scheduled_effect = null;
export let is_flushing_effect = false; export let is_flushing_effect = false;
export let is_destroying_effect = false; export let is_destroying_effect = false;
@ -229,16 +234,81 @@ export function check_dirtiness(reaction) {
} }
/** /**
* @param {Error} error * @param {unknown} error
* @param {Effect} effect
*/
function propagate_error(error, effect) {
/** @type {Effect | null} */
var current = effect;
while (current !== null) {
if ((current.f & BOUNDARY_EFFECT) !== 0) {
try {
// @ts-expect-error
current.fn(error);
return;
} catch {
// Remove boundary flag from effect
current.f ^= BOUNDARY_EFFECT;
}
}
current = current.parent;
}
is_throwing_error = false;
throw error;
}
/**
* @param {Effect} effect
*/
function should_rethrow_error(effect) {
return (
(effect.f & DESTROYED) === 0 &&
(effect.parent === null || (effect.parent.f & BOUNDARY_EFFECT) === 0)
);
}
export function reset_is_throwing_error() {
is_throwing_error = false;
}
/**
* @param {unknown} error
* @param {Effect} effect * @param {Effect} effect
* @param {Effect | null} previous_effect
* @param {ComponentContext | null} component_context * @param {ComponentContext | null} component_context
*/ */
function handle_error(error, effect, component_context) { export function handle_error(error, effect, previous_effect, component_context) {
// Given we don't yet have error boundaries, we will just always throw. if (is_throwing_error) {
if (!DEV || handled_errors.has(error) || component_context === null) { if (previous_effect === null) {
is_throwing_error = false;
}
if (should_rethrow_error(effect)) {
throw error; throw error;
} }
return;
}
if (previous_effect !== null) {
is_throwing_error = true;
}
if (
!DEV ||
component_context === null ||
!(error instanceof Error) ||
handled_errors.has(error)
) {
propagate_error(error, effect);
return;
}
handled_errors.add(error);
const component_stack = []; const component_stack = [];
const effect_name = effect.fn?.name; const effect_name = effect.fn?.name;
@ -268,6 +338,9 @@ function handle_error(error, effect, component_context) {
define_property(error, 'message', { define_property(error, 'message', {
value: error.message + `\n${component_stack.map((name) => `\n${indent}in ${name}`).join('')}\n` value: error.message + `\n${component_stack.map((name) => `\n${indent}in ${name}`).join('')}\n`
}); });
define_property(error, 'component_stack', {
value: component_stack
});
const stack = error.stack; const stack = error.stack;
@ -287,8 +360,11 @@ function handle_error(error, effect, component_context) {
}); });
} }
handled_errors.add(error); propagate_error(error, effect);
if (should_rethrow_error(effect)) {
throw error; throw error;
}
} }
/** /**
@ -449,7 +525,7 @@ export function update_effect(effect) {
dev_effect_stack.push(effect); dev_effect_stack.push(effect);
} }
} catch (error) { } catch (error) {
handle_error(/** @type {Error} */ (error), effect, previous_component_context); handle_error(error, effect, previous_effect, previous_component_context || effect.ctx);
} finally { } finally {
active_effect = previous_effect; active_effect = previous_effect;
@ -459,27 +535,47 @@ export function update_effect(effect) {
} }
} }
function log_effect_stack() {
// eslint-disable-next-line no-console
console.error(
'Last ten effects were: ',
dev_effect_stack.slice(-10).map((d) => d.fn)
);
dev_effect_stack = [];
}
function infinite_loop_guard() { function infinite_loop_guard() {
if (flush_count > 1000) { if (flush_count > 1000) {
flush_count = 0; flush_count = 0;
if (DEV) {
try { try {
e.effect_update_depth_exceeded(); e.effect_update_depth_exceeded();
} catch (error) { } catch (error) {
if (DEV) {
// stack is garbage, ignore. Instead add a console.error message. // stack is garbage, ignore. Instead add a console.error message.
define_property(error, 'stack', { define_property(error, 'stack', {
value: '' value: ''
}); });
// eslint-disable-next-line no-console }
console.error( // Try and handle the error so it can be caught at a boundary, that's
'Last ten effects were: ', // if there's an effect available from when it was last scheduled
dev_effect_stack.slice(-10).map((d) => d.fn) if (last_scheduled_effect !== null) {
); if (DEV) {
dev_effect_stack = []; try {
throw error; 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 { } else {
e.effect_update_depth_exceeded(); handle_error(error, last_scheduled_effect, null, null);
}
} else {
if (DEV) {
log_effect_stack();
}
throw error;
}
} }
} }
flush_count++; flush_count++;
@ -529,7 +625,9 @@ function flush_queued_effects(effects) {
for (var i = 0; i < length; i++) { for (var i = 0; i < length; i++) {
var effect = effects[i]; var effect = effects[i];
if ((effect.f & (DESTROYED | INERT)) === 0 && check_dirtiness(effect)) { if ((effect.f & (DESTROYED | INERT)) === 0) {
try {
if (check_dirtiness(effect)) {
update_effect(effect); update_effect(effect);
// Effects with no dependencies or teardown do not get added to the effect tree. // Effects with no dependencies or teardown do not get added to the effect tree.
@ -547,6 +645,10 @@ function flush_queued_effects(effects) {
} }
} }
} }
} catch (error) {
handle_error(error, effect, null, effect.ctx);
}
}
} }
} }
@ -558,8 +660,10 @@ function process_deferred() {
const previous_queued_root_effects = queued_root_effects; const previous_queued_root_effects = queued_root_effects;
queued_root_effects = []; queued_root_effects = [];
flush_queued_root_effects(previous_queued_root_effects); flush_queued_root_effects(previous_queued_root_effects);
if (!is_micro_task_queued) { if (!is_micro_task_queued) {
flush_count = 0; flush_count = 0;
last_scheduled_effect = null;
if (DEV) { if (DEV) {
dev_effect_stack = []; dev_effect_stack = [];
} }
@ -578,6 +682,8 @@ export function schedule_effect(signal) {
} }
} }
last_scheduled_effect = signal;
var effect = signal; var effect = signal;
while (effect.parent !== null) { while (effect.parent !== null) {
@ -612,14 +718,21 @@ function process_effects(effect, collected_effects) {
var flags = current_effect.f; var flags = current_effect.f;
var is_branch = (flags & BRANCH_EFFECT) !== 0; var is_branch = (flags & BRANCH_EFFECT) !== 0;
var is_skippable_branch = is_branch && (flags & CLEAN) !== 0; var is_skippable_branch = is_branch && (flags & CLEAN) !== 0;
var sibling = current_effect.next;
if (!is_skippable_branch && (flags & INERT) === 0) { if (!is_skippable_branch && (flags & INERT) === 0) {
if ((flags & RENDER_EFFECT) !== 0) { if ((flags & RENDER_EFFECT) !== 0) {
if (is_branch) { if (is_branch) {
current_effect.f ^= CLEAN; current_effect.f ^= CLEAN;
} else if (check_dirtiness(current_effect)) { } else {
try {
if (check_dirtiness(current_effect)) {
update_effect(current_effect); update_effect(current_effect);
} }
} catch (error) {
handle_error(error, current_effect, null, current_effect.ctx);
}
}
var child = current_effect.first; var child = current_effect.first;
@ -632,8 +745,6 @@ function process_effects(effect, collected_effects) {
} }
} }
var sibling = current_effect.next;
if (sibling === null) { if (sibling === null) {
let parent = current_effect.parent; let parent = current_effect.parent;
@ -692,6 +803,7 @@ export function flush_sync(fn) {
} }
flush_count = 0; flush_count = 0;
last_scheduled_effect = null;
if (DEV) { if (DEV) {
dev_effect_stack = []; dev_effect_stack = [];
} }
@ -810,15 +922,17 @@ export function safe_get(signal) {
} }
/** /**
* Invokes a function and captures all signals that are read during the invocation, * Capture an array of all the signals that are read when `fn` is called
* then invalidates them. * @template T
* @param {() => any} fn * @param {() => T} fn
*/ */
export function invalidate_inner_signals(fn) { export function capture_signals(fn) {
var previous_captured_signals = captured_signals; var previous_captured_signals = captured_signals;
captured_signals = new Set(); captured_signals = new Set();
var captured = captured_signals; var captured = captured_signals;
var signal; var signal;
try { try {
untrack(fn); untrack(fn);
if (previous_captured_signals !== null) { if (previous_captured_signals !== null) {
@ -829,7 +943,19 @@ export function invalidate_inner_signals(fn) {
} finally { } finally {
captured_signals = previous_captured_signals; captured_signals = previous_captured_signals;
} }
for (signal of captured) {
return captured;
}
/**
* Invokes a function and captures all signals that are read during the invocation,
* then invalidates them.
* @param {() => any} fn
*/
export function invalidate_inner_signals(fn) {
var captured = capture_signals(() => untrack(fn));
for (var signal of captured) {
// Go one level up because derived signals created as part of props in legacy mode // Go one level up because derived signals created as part of props in legacy mode
if ((signal.f & LEGACY_DERIVED_PROP) !== 0) { if ((signal.f & LEGACY_DERIVED_PROP) !== 0) {
for (const dep of /** @type {Derived} */ (signal).deps || []) { for (const dep of /** @type {Derived} */ (signal).deps || []) {
@ -984,23 +1110,32 @@ function get_parent_context(component_context) {
} }
/** /**
* @param {Value<number>} signal * @template {number | bigint} T
* @param {Value<T>} signal
* @param {1 | -1} [d] * @param {1 | -1} [d]
* @returns {number} * @returns {T}
*/ */
export function update(signal, d = 1) { export function update(signal, d = 1) {
var value = +get(signal); var value = get(signal);
set(signal, value + d); var result = d === 1 ? value++ : value--;
return value;
set(signal, value);
// @ts-expect-error
return result;
} }
/** /**
* @param {Value<number>} signal * @template {number | bigint} T
* @param {Value<T>} signal
* @param {1 | -1} [d] * @param {1 | -1} [d]
* @returns {number} * @returns {T}
*/ */
export function update_pre(signal, d = 1) { export function update_pre(signal, d = 1) {
return set(signal, +get(signal) + d); var value = get(signal);
// @ts-expect-error
return set(signal, d === 1 ? ++value : --value);
} }
/** /**

@ -5,6 +5,20 @@ import { DEV } from 'esm-env';
var bold = 'font-weight: bold'; var bold = 'font-weight: bold';
var normal = 'font-weight: normal'; var normal = 'font-weight: normal';
/**
* Assignment to `%property%` property (%location%) will evaluate to the right-hand side, not the value of `%property%` following the assignment. This may result in unexpected behaviour.
* @param {string} property
* @param {string} location
*/
export function assignment_value_stale(property, location) {
if (DEV) {
console.warn(`%c[svelte] assignment_value_stale\n%cAssignment to \`${property}\` property (${location}) will evaluate to the right-hand side, not the value of \`${property}\` following the assignment. This may result in unexpected behaviour.`, bold, normal);
} else {
// TODO print a link to the documentation
console.warn("assignment_value_stale");
}
}
/** /**
* `%binding%` (%location%) is binding to a non-reactive property * `%binding%` (%location%) is binding to a non-reactive property
* @param {string} binding * @param {string} binding
@ -153,6 +167,19 @@ export function ownership_invalid_mutation(component, owner) {
} }
} }
/**
* A `$:` statement (%location%) read reactive state that was not visible to the compiler. Updates to this state will not cause the statement to re-run. The behaviour of this code will change if you migrate it to runes mode
* @param {string} location
*/
export function reactive_declaration_non_reactive_property(location) {
if (DEV) {
console.warn(`%c[svelte] reactive_declaration_non_reactive_property\n%cA \`$:\` statement (${location}) read reactive state that was not visible to the compiler. Updates to this state will not cause the statement to re-run. The behaviour of this code will change if you migrate it to runes mode`, bold, normal);
} else {
// TODO print a link to the documentation
console.warn("reactive_declaration_non_reactive_property");
}
}
/** /**
* Reactive `$state(...)` proxies and the values they proxy have different identities. Because of this, comparisons with `%operator%` will produce unexpected results * Reactive `$state(...)` proxies and the values they proxy have different identities. Because of this, comparisons with `%operator%` will produce unexpected results
* @param {string} operator * @param {string} operator

@ -34,12 +34,11 @@ function stringify(element) {
/** /**
* @param {Payload} payload * @param {Payload} payload
* @param {Element} parent * @param {string} message
* @param {Element} child
*/ */
function print_error(payload, parent, child) { function print_error(payload, message) {
var message = message =
`node_invalid_placement_ssr: ${stringify(parent)} cannot contain ${stringify(child)}\n\n` + `node_invalid_placement_ssr: ${message}\n\n` +
'This can cause content to shift around as the browser repairs the HTML, and will likely result in a `hydration_mismatch` warning.'; 'This can cause content to shift around as the browser repairs the HTML, and will likely result in a `hydration_mismatch` warning.';
if ((seen ??= new Set()).has(message)) return; if ((seen ??= new Set()).has(message)) return;
@ -72,15 +71,23 @@ export function push_element(payload, tag, line, column) {
var ancestor = parent.parent; var ancestor = parent.parent;
var ancestors = [parent.tag]; var ancestors = [parent.tag];
if (!is_tag_valid_with_parent(tag, parent.tag)) { const child_loc = filename ? `${filename}:${line}:${column}` : undefined;
print_error(payload, parent, child); const parent_loc = parent.filename
} ? `${parent.filename}:${parent.line}:${parent.column}`
: undefined;
const message = is_tag_valid_with_parent(tag, parent.tag, child_loc, parent_loc);
if (message) print_error(payload, message);
while (ancestor != null) { while (ancestor != null) {
ancestors.push(ancestor.tag); ancestors.push(ancestor.tag);
if (!is_tag_valid_with_ancestor(tag, ancestors)) { const ancestor_loc = ancestor.filename
print_error(payload, ancestor, child); ? `${ancestor.filename}:${ancestor.line}:${ancestor.column}`
} : undefined;
const message = is_tag_valid_with_ancestor(tag, ancestors, child_loc, ancestor_loc);
if (message) print_error(payload, message);
ancestor = ancestor.parent; ancestor = ancestor.parent;
} }
} }

@ -231,11 +231,13 @@ export function spread_attributes(attrs, classes, styles, flags = 0) {
if (name[0] === '$' && name[1] === '$') continue; // faster than name.startsWith('$$') if (name[0] === '$' && name[1] === '$') continue; // faster than name.startsWith('$$')
if (INVALID_ATTR_NAME_CHAR_REGEX.test(name)) continue; if (INVALID_ATTR_NAME_CHAR_REGEX.test(name)) continue;
var value = attrs[name];
if (lowercase) { if (lowercase) {
name = name.toLowerCase(); name = name.toLowerCase();
} }
attr_str += attr(name, attrs[name], is_html && is_boolean_attribute(name)); attr_str += attr(name, value, is_html && is_boolean_attribute(name));
} }
return attr_str; return attr_str;

@ -1,2 +1,32 @@
import { MediaQuery } from 'svelte/reactivity';
export * from './spring.js'; export * from './spring.js';
export * from './tweened.js'; export * from './tweened.js';
/**
* A [media query](https://svelte.dev/docs/svelte/svelte-reactivity#MediaQuery) that matches if the user [prefers reduced motion](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-reduced-motion).
*
* ```svelte
* <script>
* import { prefersReducedMotion } from 'svelte/motion';
* import { fly } from 'svelte/transition';
*
* let visible = $state(false);
* </script>
*
* <button onclick={() => visible = !visible}>
* toggle
* </button>
*
* {#if visible}
* <p transition:fly={{ y: prefersReducedMotion.current ? 0 : 200 }}>
* flies in, unless the user prefers reduced motion
* </p>
* {/if}
* ```
* @type {MediaQuery}
* @since 5.7.0
*/
export const prefersReducedMotion = /*@__PURE__*/ new MediaQuery(
'(prefers-reduced-motion: reduce)'
);

@ -0,0 +1,81 @@
import { get, tick, untrack } from '../internal/client/runtime.js';
import { effect_tracking, render_effect } from '../internal/client/reactivity/effects.js';
import { source } from '../internal/client/reactivity/sources.js';
import { increment } from './utils.js';
/**
* Returns a `subscribe` function that, if called in an effect (including expressions in the template),
* calls its `start` callback with an `update` function. Whenever `update` is called, the effect re-runs.
*
* If `start` returns a function, it will be called when the effect is destroyed.
*
* If `subscribe` is called in multiple effects, `start` will only be called once as long as the effects
* are active, and the returned teardown function will only be called when all effects are destroyed.
*
* It's best understood with an example. Here's an implementation of [`MediaQuery`](https://svelte.dev/docs/svelte/svelte-reactivity#MediaQuery):
*
* ```js
* import { createSubscriber } from 'svelte/reactivity';
* import { on } from 'svelte/events';
*
* export class MediaQuery {
* #query;
* #subscribe;
*
* constructor(query) {
* this.#query = window.matchMedia(`(${query})`);
*
* this.#subscribe = createSubscriber((update) => {
* // when the `change` event occurs, re-run any effects that read `this.current`
* const off = on(this.#query, 'change', update);
*
* // stop listening when all the effects are destroyed
* return () => off();
* });
* }
*
* get current() {
* this.#subscribe();
*
* // Return the current state of the query, whether or not we're in an effect
* return this.#query.matches;
* }
* }
* ```
* @param {(update: () => void) => (() => void) | void} start
* @since 5.7.0
*/
export function createSubscriber(start) {
let subscribers = 0;
let version = source(0);
/** @type {(() => void) | void} */
let stop;
return () => {
if (effect_tracking()) {
get(version);
render_effect(() => {
if (subscribers === 0) {
stop = untrack(() => start(() => increment(version)));
}
subscribers += 1;
return () => {
tick().then(() => {
// Only count down after timeout, else we would reach 0 before our own render effect reruns,
// but reach 1 again when the tick callback of the prior teardown runs. That would mean we
// re-subcribe unnecessarily and create a memory leak because the old subscription is never cleaned up.
subscribers -= 1;
if (subscribers === 0) {
stop?.();
stop = undefined;
}
});
};
});
}
};
}

@ -1,8 +1,7 @@
/** @import { Source } from '#client' */ /** @import { Source } from '#client' */
import { DESTROYED } from '../internal/client/constants.js';
import { derived } from '../internal/client/index.js'; import { derived } from '../internal/client/index.js';
import { source, set } from '../internal/client/reactivity/sources.js'; import { source, set } from '../internal/client/reactivity/sources.js';
import { get } from '../internal/client/runtime.js'; import { active_reaction, get, set_active_reaction } from '../internal/client/runtime.js';
var inited = false; var inited = false;
@ -12,6 +11,8 @@ export class SvelteDate extends Date {
/** @type {Map<keyof Date, Source<unknown>>} */ /** @type {Map<keyof Date, Source<unknown>>} */
#deriveds = new Map(); #deriveds = new Map();
#reaction = active_reaction;
/** @param {any[]} params */ /** @param {any[]} params */
constructor(...params) { constructor(...params) {
// @ts-ignore // @ts-ignore
@ -43,7 +44,12 @@ export class SvelteDate extends Date {
var d = this.#deriveds.get(method); var d = this.#deriveds.get(method);
if (d === undefined || (d.f & DESTROYED) !== 0) { if (d === undefined) {
// lazily create the derived, but as though it were being
// created at the same time as the class instance
const reaction = active_reaction;
set_active_reaction(this.#reaction);
d = derived(() => { d = derived(() => {
get(this.#time); get(this.#time);
// @ts-ignore // @ts-ignore
@ -51,6 +57,8 @@ export class SvelteDate extends Date {
}); });
this.#deriveds.set(method, d); this.#deriveds.set(method, d);
set_active_reaction(reaction);
} }
return get(d); return get(d);

@ -642,3 +642,33 @@ test('Date methods invoked for the first time in a derived', () => {
cleanup(); cleanup();
}); });
test('Date methods shared between deriveds', () => {
const date = new SvelteDate(initial_date);
const log: any = [];
const cleanup = effect_root(() => {
const year = derived(() => {
return date.getFullYear();
});
const year2 = derived(() => {
return date.getTime(), date.getFullYear();
});
render_effect(() => {
log.push(get(year) + '/' + get(year2).toString());
});
flushSync(() => {
date.setFullYear(date.getFullYear() + 1);
});
flushSync(() => {
date.setFullYear(date.getFullYear() + 1);
});
});
assert.deepEqual(log, ['2023/2023', '2024/2024', '2025/2025']);
cleanup();
});

@ -3,3 +3,5 @@ export { SvelteSet } from './set.js';
export { SvelteMap } from './map.js'; export { SvelteMap } from './map.js';
export { SvelteURL } from './url.js'; export { SvelteURL } from './url.js';
export { SvelteURLSearchParams } from './url-search-params.js'; export { SvelteURLSearchParams } from './url-search-params.js';
export { MediaQuery } from './media-query.js';
export { createSubscriber } from './create-subscriber.js';

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

Loading…
Cancel
Save