Merge branch 'main' into svelte-html

svelte-html
Simon Holthausen 3 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:
```js
// @filename: ambient.d.ts
declare global {
const todos: Array<{ done: boolean, text: string }>
}
// @filename: index.js
let todos = [{ done: false, text: 'add more todos' }];
// ---cut---
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 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
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`
@ -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`.
## 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 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
```svelte

@ -246,6 +246,23 @@ We can tighten things up further by declaring a generic, so that `data` and `row
</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
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`.
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>`
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>
```
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>`
Inputs that work together can use `bind:group`.
```svelte
<script>
let tortilla = 'Plain';
let tortilla = $state('Plain');
/** @type {Array<string>} */
let fillings = [];
let fillings = $state([]);
</script>
<!-- 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>
```
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>` 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.
### 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
/// file: svelte.config.js
// @noErrors
import { vitePreprocess } from '@sveltejs/kit/vite';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
const config = {
preprocess: vitePreprocess()
// Note the additional `{ script: true }`
preprocess: vitePreprocess({ script: true })
};
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
/// file: svelte.config.js
// @noErrors
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
const config = {
@ -69,6 +69,8 @@ const 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.
### 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
## 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`
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.
`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.
### `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).
## 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?
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! -->
### 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
```
@ -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
```
### 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
```

@ -400,6 +400,12 @@ Expected token %token%
Expected whitespace
```
### export_undefined
```
`%name%` is not defined
```
### global_reference_invalid
```
@ -481,12 +487,12 @@ A component cannot have a default export
### 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:
- `<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)
- `<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
```
### 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
```
@ -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_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
```

@ -643,12 +643,12 @@ Svelte 5 components are no longer classes. Instantiate them using `mount` or `hy
### 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:
- `<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)
- `<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
```
### 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
```

@ -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.
The usual shorthand rules apply — `let:item` is equivalent to `let:item={item}`, and `<slot {item}>` is equivalent to `<slot item={item}>`.
```svelte
<!-- FancyList.svelte -->
<!--- file: FancyList.svelte --->
<ul>
{#each items as item}
{#each items as data}
<li class="fancy">
<slot prop={item} />
<!-- 'item' here... -->
<slot item={process(data)} />
</li>
{/each}
</ul>
```
<!-- App.svelte -->
<FancyList {items} let:prop={thing}>
<div>{thing.text}</div>
```svelte
<!--- file: App.svelte --->
<!-- ...corresponds to 'item' here: -->
<FancyList {items} let:item={processed}>
<div>{processed.text}</div>
</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.
```svelte
<!-- FancyList.svelte -->
<!--- file: FancyList.svelte --->
<ul>
{#each items as item}
<li class="fancy">
<slot name="item" {item} />
<slot name="item" item={process(data)} />
</li>
{/each}
</ul>
<slot name="footer" />
```
<!-- App.svelte -->
```svelte
<!--- file: App.svelte --->
<FancyList {items}>
<div slot="item" let:item>{item.text}</div>
<p slot="footer">Copyright (c) 2019 Svelte Industries</p>

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

@ -1,5 +1,143 @@
# 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
### Patch Changes

@ -1103,6 +1103,11 @@ export interface HTMLInputAttributes extends HTMLAttributes<HTMLInputElement> {
step?: number | string | undefined | null;
type?: HTMLInputTypeAttribute | undefined | null;
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;
webkitdirectory?: boolean | undefined | null;
@ -1384,6 +1389,9 @@ export interface HTMLTextareaAttributes extends HTMLAttributes<HTMLTextAreaEleme
required?: boolean | undefined | null;
rows?: 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;
'on:change'?: ChangeEventHandler<HTMLTextAreaElement> | undefined | null;
@ -2045,6 +2053,10 @@ export interface SvelteHTMLElements {
[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 };
}

@ -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%` 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
## 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
> 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
## export_undefined
> `%name%` is not defined
## global_reference_invalid
> `%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
## 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
> Cannot reassign or bind to snippet parameter

@ -190,11 +190,11 @@
## 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:
- `<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)
- `<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_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
> Invalid component definition — must be an `{expression}`

@ -26,10 +26,6 @@
> 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 in its own scope will never update. Did you mean to reference it inside a closure?

@ -40,11 +40,11 @@
## 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:
- `<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)
- `<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",
"description": "Cybernetically enhanced web apps",
"license": "MIT",
"version": "5.2.9",
"version": "5.7.1",
"type": "module",
"types": "./types/index.d.ts",
"engines": {
@ -142,8 +142,8 @@
"acorn-typescript": "^1.4.13",
"aria-query": "^5.3.1",
"axobject-query": "^4.1.0",
"esm-env": "^1.0.0",
"esrap": "^1.2.2",
"esm-env": "^1.2.1",
"esrap": "^1.2.3",
"is-reference": "^3.0.3",
"locate-character": "^3.0.0",
"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");
}
/**
* `%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%`
* @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`);
}
/**
* 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
* @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 {string} thing
* @param {string} parent
* @param {string} message
* @returns {never}
*/
export function node_invalid_placement(node, thing, parent) {
e(node, "node_invalid_placement", `${thing} is invalid inside \`<${parent}>\``);
export function node_invalid_placement(node, message) {
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");
}
/**
* 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}`
* @param {null | number | NodeLike} node

@ -381,6 +381,20 @@ export function convert(source, ast) {
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 }) {
return {
type: 'Element',

@ -10,17 +10,43 @@ const ParserWithTS = acorn.Parser.extend(tsPlugin({ allowSatisfies: true }));
/**
* @param {string} source
* @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 { onComment, add_comments } = get_comment_handlers(source);
const ast = parser.parse(source, {
onComment,
sourceType: 'module',
ecmaVersion: 13,
locations: true
});
// @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;
try {
ast = parser.parse(source, {
onComment,
sourceType: 'module',
ecmaVersion: 13,
locations: true
});
} finally {
if (is_script) {
// @ts-ignore
parser.prototype.parseStatement = parse_statement;
}
}
if (typescript) amend(source, ast);
add_comments(ast);

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

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

@ -44,7 +44,8 @@ const meta_tags = new Map([
['svelte:element', 'SvelteElement'],
['svelte:component', 'SvelteComponent'],
['svelte:self', 'SvelteSelf'],
['svelte:fragment', 'SvelteFragment']
['svelte:fragment', 'SvelteFragment'],
['svelte:boundary', 'SvelteBoundary']
]);
/** @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 { Parser } from '../index.js' */
import read_pattern from '../read/context.js';
@ -142,16 +142,25 @@ function open(parser) {
parser.index = end;
}
}
parser.eat('as', true);
parser.require_whitespace();
const context = read_pattern(parser);
parser.allow_whitespace();
/** @type {Pattern | null} */
let context = null;
let index;
let key;
if (parser.eat('as')) {
parser.require_whitespace();
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();
if (parser.eat(',')) {
parser.allow_whitespace();
index = parser.read_identifier();
@ -314,7 +323,11 @@ function open(parser) {
name
},
parameters: function_expression.params,
body: create_fragment()
body: create_fragment(),
metadata: {
can_hoist: false,
sites: new Set()
}
});
parser.stack.push(block);
parser.fragments.push(block.body);
@ -605,7 +618,8 @@ function special(parser) {
metadata: {
dynamic: false,
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 { walk } from 'zimmerframe';
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 { 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 */
const NODE_PROBABLY_EXISTS = 0;
@ -50,81 +43,43 @@ 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.AST.RegularElement | Compiler.AST.SvelteElement | Compiler.AST.RenderTag} element
* @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement} element
*/
export function prune(stylesheet, element) {
if (element.type === 'RenderTag') {
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) {
if (node.metadata.is_global_block) {
context.visit(node.prelude);
} else {
context.next();
}
},
ComplexSelector(node, context) {
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();
walk(/** @type {Compiler.Css.Node} */ (stylesheet), null, {
Rule(node, context) {
if (node.metadata.is_global_block) {
context.visit(node.prelude);
} else {
context.next();
}
},
ComplexSelector(node) {
const selectors = get_relative_selectors(node);
while (selectors_to_check.length > 0) {
selectors_to_check.pop();
seen.clear();
if (
apply_selector(
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);
if (
apply_selector(selectors, /** @type {Compiler.Css.Rule} */ (node.metadata.rule), element)
) {
node.metadata.used = true;
}
} 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;
}
// note: we don't call context.next() here, we only recurse into
// selectors that don't belong to rules (i.e. inside `:is(...)` etc)
// when we encounter them below
}
};
// note: we don't call context.next() here, we only recurse into
// selectors that don't belong to rules (i.e. inside `:is(...)` etc)
// when we encounter them below
}
});
}
/**
* 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
for (let selector of selectors) {
walk(
selector,
{},
{
// @ts-ignore
NestingSelector() {
has_explicit_nesting_selector = true;
}
walk(selector, null, {
// @ts-ignore
NestingSelector() {
has_explicit_nesting_selector = true;
}
);
});
// if we found one we can break from the others
if (has_explicit_nesting_selector) break;
}
@ -203,89 +155,69 @@ function truncate(node) {
* @param {Compiler.Css.RelativeSelector[]} relative_selectors
* @param {Compiler.Css.Rule} rule
* @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement} element
* @param {State} state
* @returns {boolean}
*/
function apply_selector(relative_selectors, rule, element, state) {
function apply_selector(relative_selectors, rule, element) {
const parent_selectors = relative_selectors.slice();
const relative_selector = parent_selectors.pop();
if (!relative_selector) return false;
const possible_match = relative_selector_might_apply_to_node(
relative_selector,
rule,
element,
state
);
if (!possible_match) {
return false;
}
const matched =
!!relative_selector &&
relative_selector_might_apply_to_node(relative_selector, rule, element) &&
apply_combinator(relative_selector, parent_selectors, rule, element);
if (relative_selector.combinator) {
return apply_combinator(
relative_selector.combinator,
relative_selector,
parent_selectors,
rule,
element,
state
);
}
if (matched) {
if (!is_outer_global(relative_selector)) {
relative_selector.metadata.scoped = true;
}
// if this is the left-most non-global selector, mark it — we want
// `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);
element.metadata.scoped = true;
}
return true;
return matched;
}
/**
* @param {Compiler.Css.Combinator} combinator
* @param {Compiler.Css.RelativeSelector} relative_selector
* @param {Compiler.Css.RelativeSelector[]} parent_selectors
* @param {Compiler.Css.Rule} rule
* @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement} element
* @param {State} state
* @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement | Compiler.AST.RenderTag | Compiler.AST.Component | Compiler.AST.SvelteComponent | Compiler.AST.SvelteSelf} node
* @returns {boolean}
*/
function apply_combinator(combinator, relative_selector, parent_selectors, rule, element, state) {
const name = combinator.name;
function apply_combinator(relative_selector, parent_selectors, rule, node) {
if (!relative_selector.combinator) return true;
const name = relative_selector.combinator.name;
switch (name) {
case ' ':
case '>': {
let parent_matched = false;
let crossed_component_boundary = false;
const path = element.metadata.path;
const path = node.metadata.path;
let i = path.length;
while (i--) {
const parent = path[i];
if (parent.type === 'Component' || parent.type === 'SvelteComponent') {
crossed_component_boundary = true;
}
if (parent.type === 'SnippetBlock') {
// We assume the snippet might be rendered in a place where the parent selectors match.
// (We could do more static analysis and check the render tag reference to see if this snippet block continues
// with elements that actually match the selector, but that would be a lot of work for little gain)
return true;
}
if (seen.has(parent)) {
parent_matched = true;
} else {
seen.add(parent);
if (parent.type === 'RegularElement' || parent.type === 'SvelteElement') {
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);
for (const site of parent.metadata.sites) {
if (apply_combinator(relative_selector, parent_selectors, rule, site)) {
parent_matched = true;
}
}
}
break;
}
if (parent.type === 'RegularElement' || parent.type === 'SvelteElement') {
if (apply_selector(parent_selectors, rule, parent)) {
parent_matched = true;
}
@ -298,7 +230,7 @@ function apply_combinator(combinator, relative_selector, parent_selectors, rule,
case '+':
case '~': {
const siblings = get_possible_element_siblings(element, name === '+');
const siblings = get_possible_element_siblings(node, name === '+');
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') {
// `{@render foo()}<p>foo</p>` with `:global(.x) + p` is a match
if (parent_selectors.length === 1 && parent_selectors[0].metadata.is_global) {
mark(relative_selector, element);
sibling_matched = true;
}
} else if (apply_selector(parent_selectors, rule, possible_sibling, state)) {
mark(relative_selector, element);
} else if (apply_selector(parent_selectors, rule, possible_sibling)) {
sibling_matched = true;
}
}
return (
sibling_matched ||
(get_element_parent(element) === null &&
(get_element_parent(node) === null &&
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
* 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.Rule} rule
* @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
const has_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,
// 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 =
rules.some((r) => r.prelude.children.some((c) => c.children.some((s) => is_global(s, r)))) ||
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);
}
walk(
/** @type {Compiler.SvelteNode} */ (element.fragment),
{ is_child: true },
{
const seen = new Set();
/**
* @param {Compiler.SvelteNode} node
* @param {{ is_child: boolean }} state
*/
function walk_children(node, state) {
walk(node, state, {
_(node, context) {
if (node.type === 'RegularElement' || node.type === 'SvelteElement') {
descendant_elements.push(node);
@ -449,12 +369,21 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element,
} else {
context.next();
}
} else if (node.type === 'RenderTag') {
for (const snippet of node.metadata.snippets) {
if (seen.has(snippet)) continue;
seen.add(snippet);
walk_children(snippet.body, context.state);
}
} else {
context.next();
}
}
}
);
});
}
walk_children(element.fragment, { is_child: true });
// :has(...) is special in that it means "look downwards in the CSS tree". Since our matching algorithm goes
// upwards and back-to-front, we need to first check the selectors inside :has(...), then check the rest of the
@ -490,7 +419,7 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element,
if (
selectors.length === 0 /* is :global(...) */ ||
(element.metadata.scoped && selector_matched) ||
apply_selector(selectors, rule, element, state)
apply_selector(selectors, rule, element)
) {
complex_selector.metadata.used = 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 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
@ -569,7 +498,7 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element,
if (is_global) {
complex_selector.metadata.used = true;
matched = true;
} else if (apply_selector(relative, rule, element, state)) {
} else if (apply_selector(relative, rule, element)) {
complex_selector.metadata.used = true;
matched = true;
} 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) {
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.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
// 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, {
RegularElement(node) {
siblings.push(node);
},
SvelteElement(node) {
siblings.push(node);
},
RenderTag(node) {
for (const snippet of node.metadata.snippets) {
if (seen.has(snippet)) continue;
seen.add(snippet);
get_siblings(snippet.body);
}
}
});
}
for (const node of nodes.slice(nodes.indexOf(start) + 1)) {
get_siblings(node);
}
if (include_self) {
siblings.push(element);
}
@ -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}
*/
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 {Set<Compiler.AST.SnippetBlock>} seen
* @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>} */
const result = new Map();
const path = element.metadata.path;
const path = node.metadata.path;
/** @type {Compiler.SvelteNode} */
let current = element;
let current = node;
let i = path.length;
@ -933,7 +879,31 @@ function get_possible_element_siblings(element, adjacent_only) {
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) {
// `{#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
*/
export function get_parent_rules(rule) {
const parents = [];
const rules = [];
let parent = rule?.metadata.parent_rule;
while (parent) {
parents.push(parent);
parent = parent.metadata.parent_rule;
while (rule) {
rules.push(rule);
rule = rule.metadata.parent_rule;
}
return parents;
return rules;
}
/**

@ -51,6 +51,7 @@ import { RenderTag } from './visitors/RenderTag.js';
import { SlotElement } from './visitors/SlotElement.js';
import { SnippetBlock } from './visitors/SnippetBlock.js';
import { SpreadAttribute } from './visitors/SpreadAttribute.js';
import { SpreadElement } from './visitors/SpreadElement.js';
import { StyleDirective } from './visitors/StyleDirective.js';
import { SvelteBody } from './visitors/SvelteBody.js';
import { SvelteComponent } from './visitors/SvelteComponent.js';
@ -61,6 +62,7 @@ import { SvelteHead } from './visitors/SvelteHead.js';
import { SvelteHTML } from './visitors/SvelteHTML.js';
import { SvelteSelf } from './visitors/SvelteSelf.js';
import { SvelteWindow } from './visitors/SvelteWindow.js';
import { SvelteBoundary } from './visitors/SvelteBoundary.js';
import { TaggedTemplateExpression } from './visitors/TaggedTemplateExpression.js';
import { Text } from './visitors/Text.js';
import { TitleElement } from './visitors/TitleElement.js';
@ -163,6 +165,7 @@ const visitors = {
SlotElement,
SnippetBlock,
SpreadAttribute,
SpreadElement,
StyleDirective,
SvelteBody,
SvelteComponent,
@ -173,6 +176,7 @@ const visitors = {
SvelteHTML,
SvelteSelf,
SvelteWindow,
SvelteBoundary,
TaggedTemplateExpression,
Text,
TransitionDirective,
@ -427,7 +431,6 @@ export function analyze_component(root, source, options) {
reactive_statements: new Map(),
binding_groups: new Map(),
slot_names: new Map(),
top_level_snippets: [],
css: {
ast: root.css,
hash: root.css
@ -440,7 +443,10 @@ export function analyze_component(root, source, options) {
: '',
keyframes: []
},
source
source,
undefined_exports: new Map(),
snippet_renderers: new Map(),
snippets: new Set()
};
if (!runes) {
@ -693,6 +699,17 @@ export function analyze_component(root, source, options) {
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) {
e.mixed_event_handler_syntaxes(
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 (
analysis.uses_render_tags &&
(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) {
if (node.type === 'RenderTag') continue;
if (node.metadata.scoped) {
// Dynamic elements in dom mode always use spread for attributes and therefore shouldn't have a class attribute added to them
// TODO this happens during the analysis phase, which shouldn't know anything about client vs server

@ -16,7 +16,7 @@ export function EachBlock(node, context) {
validate_block_not_empty(node.fallback, 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
e.state_invalid_placement(node, id.name);
}

@ -9,11 +9,12 @@ import { mark_subtree_dynamic } from './shared/fragment.js';
* @param {Context} 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 (!is_tag_valid_with_parent('#text', context.state.parent_element)) {
e.node_invalid_placement(node, '`{expression}`', context.state.parent_element);
if (in_template && context.state.parent_element) {
const message = is_tag_valid_with_parent('#text', 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;
}
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();
}

@ -114,15 +114,12 @@ export function RegularElement(node, context) {
if (!past_parent) {
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) {
w.node_invalid_placement_ssr(
node,
`\`<${node.name}>\``,
context.state.parent_element
);
w.node_invalid_placement_ssr(node, message);
} 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') {
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) {
w.node_invalid_placement_ssr(node, `\`<${node.name}>\``, ancestor.name);
w.node_invalid_placement_ssr(node, message);
} else {
e.node_invalid_placement(node, `\`<${node.name}>\``, ancestor.name);
e.node_invalid_placement(node, message);
}
}
} else if (

@ -4,6 +4,7 @@ import { unwrap_optional } from '../../../utils/ast.js';
import * as e from '../../../errors.js';
import { validate_opening_tag } from './shared/utils.js';
import { mark_subtree_dynamic } from './shared/fragment.js';
import { is_resolved_snippet } from './shared/snippets.js';
/**
* @param {AST.RenderTag} node
@ -13,13 +14,25 @@ export function RenderTag(node, context) {
validate_opening_tag(node, context.state, '@');
node.metadata.path = [...context.path];
context.state.analysis.elements.push(node);
const callee = unwrap_optional(node.expression).callee;
node.metadata.dynamic =
callee.type !== 'Identifier' || context.state.scope.get(callee.name)?.kind !== 'normal';
const binding = callee.type === 'Identifier' ? context.state.scope.get(callee.name) : null;
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;
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 { validate_block_not_empty, validate_opening_tag } from './shared/utils.js';
import * as e from '../../../errors.js';
@ -8,6 +9,8 @@ import * as e from '../../../errors.js';
* @param {Context} context
*/
export function SnippetBlock(node, context) {
context.state.analysis.snippets.add(node);
validate_block_not_empty(node.body, context);
if (context.state.analysis.runes) {
@ -22,6 +25,25 @@ export function SnippetBlock(node, context) {
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 parent = path.at(-2);
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
*/
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 (!is_tag_valid_with_parent('#text', context.state.parent_element)) {
e.node_invalid_placement(node, 'Text node', context.state.parent_element);
if (in_template && context.state.parent_element && regex_not_whitespace.test(node.data)) {
const message = is_tag_valid_with_parent('#text', context.state.parent_element);
if (message) {
e.node_invalid_placement(node, message);
}
}
}

@ -9,12 +9,63 @@ import {
validate_slot_attribute
} from './attribute.js';
import { mark_subtree_dynamic } from './fragment.js';
import { is_resolved_snippet } from './snippets.js';
/**
* @param {AST.Component | AST.SvelteComponent | AST.SvelteSelf} node
* @param {Context} 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);
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 { SvelteElement } from './visitors/SvelteElement.js';
import { SvelteFragment } from './visitors/SvelteFragment.js';
import { SvelteBoundary } from './visitors/SvelteBoundary.js';
import { SvelteHead } from './visitors/SvelteHead.js';
import { SvelteHTML } from './visitors/SvelteHTML.js';
import { SvelteSelf } from './visitors/SvelteSelf.js';
@ -123,6 +124,7 @@ const visitors = {
SvelteDocument,
SvelteElement,
SvelteFragment,
SvelteBoundary,
SvelteHead,
SvelteHTML,
SvelteSelf,
@ -165,6 +167,8 @@ export function client_component(analysis, options) {
private_state: new Map(),
transform: {},
in_constructor: false,
instance_level_snippets: [],
module_level_snippets: [],
// these are set inside the `Fragment` visitor, and cannot be used until then
before_init: /** @type {any} */ (null),
@ -370,7 +374,7 @@ export function client_component(analysis, options) {
...store_setup,
...legacy_reactive_declarations,
...group_binding_declarations,
...analysis.top_level_snippets,
...state.instance_level_snippets,
.../** @type {ESTree.Statement[]} */ (instance.body),
analysis.runes || !analysis.needs_context
? 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(
b.id(analysis.name),

@ -6,7 +6,8 @@ import type {
PrivateIdentifier,
Expression,
AssignmentExpression,
UpdateExpression
UpdateExpression,
VariableDeclaration
} from 'estree';
import type { Namespace, SvelteNode, ValidatedCompileOptions } from '#compiler';
import type { TransformState } from '../types.js';
@ -31,7 +32,7 @@ export interface ClientTransformState extends TransformState {
/** turn `foo = bar` into e.g. `$.set(foo, bar)` */
assign?: (node: Identifier, value: Expression) => Expression;
/** 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)` */
update?: (node: UpdateExpression) => Expression;
}
@ -85,6 +86,11 @@ export interface ComponentClientTransformState extends ClientTransformState {
/** The $: calls, which will be ordered in the end */
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 {

@ -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 * as b from '../../../../utils/builders.js';
import { build_assignment_value } from '../../../../utils/ast.js';
import { is_ignored } from '../../../../state.js';
import {
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 { visit_assignment_expression } from '../../shared/assignments.js';
@ -20,6 +26,24 @@ export function AssignmentExpression(node, context) {
: 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 {Pattern} left
@ -41,7 +65,11 @@ function build_assignment(operator, left, right, context) {
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));
}
@ -73,24 +101,28 @@ function build_assignment(operator, left, right, context) {
? context.state.transform[object.name]
: null;
const path = context.path.map((node) => node.type);
// reassignment
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} */ (
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 (
!is_primitive &&
binding.kind !== 'prop' &&
binding.kind !== 'bindable_prop' &&
binding.kind !== 'raw_state' &&
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);
@ -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;
}

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

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

@ -9,23 +9,44 @@ import * as b from '../../../../utils/builders.js';
*/
export function IfBlock(node, context) {
context.state.template.push('<!>');
const statements = [];
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 = [
context.state.node,
b.thunk(/** @type {Expression} */ (context.visit(node.test))),
b.arrow([b.id('$$anchor')], consequent)
b.arrow(
[b.id('$$render')],
b.block([
b.if(
/** @type {Expression} */ (context.visit(node.test)),
b.stmt(b.call(b.id('$$render'), b.id(consequent_id))),
alternate_id
? b.stmt(
b.call(
b.id('$$render'),
b.id(alternate_id),
node.alternate ? b.literal(false) : undefined
)
)
: undefined
)
])
)
];
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) {
// We treat this...
//
@ -51,5 +72,7 @@ export function IfBlock(node, context) {
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 { ReactiveStatement } from '#compiler' */
/** @import { ComponentContext } from '../types' */
import { dev, is_ignored, locator } from '../../../../state.js';
import * as b from '../../../../utils/builders.js';
import { build_getter } from '../utils.js';
@ -48,6 +50,11 @@ export function LabeledStatement(node, context) {
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
context.state.legacy_reactive_statements.set(
node,
@ -55,7 +62,9 @@ export function LabeledStatement(node, context) {
b.call(
'$.legacy_pre_effect',
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(
'$.store_mutate',
get_store(),
b.assignment(
mutation.operator,
/** @type {MemberExpression} */ (
replace(/** @type {MemberExpression} */ (mutation.left))
),
mutation.right
),
mutation.type === 'AssignmentExpression'
? b.assignment(
mutation.operator,
/** @type {MemberExpression} */ (
replace(/** @type {MemberExpression} */ (mutation.left))
),
mutation.right
)
: b.update(mutation.operator, replace(mutation.argument), mutation.prefix),
untracked
);
},

@ -172,20 +172,28 @@ export function RegularElement(node, context) {
}
}
if (
node.name === 'input' &&
(has_spread ||
bindings.has('value') ||
bindings.has('checked') ||
bindings.has('group') ||
attributes.some(
(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)));
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 (
!has_default_value_attribute &&
(has_spread ||
bindings.has('value') ||
bindings.has('checked') ||
bindings.has('group') ||
(!bindings.has('group') && has_value_attribute))
) {
context.state.init.push(b.stmt(b.call('$.remove_input_defaults', context.state.node)));
}
}
if (node.name === 'textarea') {
@ -283,7 +291,7 @@ export function RegularElement(node, context) {
const is = is_custom_element
? 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;
}
}
@ -511,10 +519,17 @@ function setup_select_synchronization(value_binding, context) {
* @param {AST.RegularElement} element
* @param {Identifier} node_id
* @param {AST.Attribute} attribute
* @param {Array<AST.Attribute | AST.SpreadAttribute>} attributes
* @param {ComponentContext} context
* @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 name = get_attribute_name(element, attribute);
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));
} else if (name === 'checked') {
update = b.stmt(b.call('$.set_checked', node_id, value));
} else if (name === 'selected') {
update = b.stmt(b.call('$.set_selected', node_id, value));
} else if (
// If 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)) {
update = b.stmt(b.assignment('=', b.member(node_id, name), value));
} else {

@ -83,7 +83,11 @@ export function SnippetBlock(node, context) {
// Top-level snippets are hoisted so they can be referenced in the `<script>`
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 {
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 { is_ignored } from '../../../../state.js';
import { object } from '../../../../utils/ast.js';
@ -34,34 +34,22 @@ export function UpdateExpression(node, context) {
}
const left = object(argument);
if (left === null) return context.next();
const transformers = left && context.state.transform[left.name];
if (left === argument) {
const transform = context.state.transform;
const update = transform[left.name]?.update;
if (update && Object.hasOwn(transform, left.name)) {
return update(node);
}
if (left === argument && transformers?.update) {
// we don't need to worry about ownership_invalid_mutation here, because
// we're not mutating but reassigning
return transformers.update(node);
}
const assignment = /** @type {Expression} */ (
context.visit(
b.assignment(
node.operator === '++' ? '+=' : '-=',
/** @type {Pattern} */ (argument),
b.literal(1)
)
)
);
let update = /** @type {Expression} */ (context.next());
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 =
node.prefix || is_standalone
? assignment
: b.binary(node.operator === '++' ? '-' : '+', assignment, b.literal(1));
if (left && transformers?.mutate) {
update = transformers.mutate(
left,
/** @type {AssignmentExpression | UpdateExpression} */ (update)
);
}
return is_ignored(node, 'ownership_invalid_mutation')
? b.call('$.skip_ownership_validation', b.thunk(update))

@ -20,7 +20,10 @@ export function UseDirective(node, context) {
context.state.node,
b.arrow(
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 { ComponentContext } from '../../types.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 { create_derived } from '../../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) {
/** @type {Array<Property[] | Expression>} */
const props_and_spreads = [];
/** @type {Array<() => void>} */
const delayed_props = [];
/** @type {ExpressionStatement[]} */
const lets = [];
@ -63,14 +65,23 @@ export function build_component(node, component_name, context, anchor = context.
/**
* @param {Property} prop
* @param {boolean} [delay]
*/
function push_prop(prop) {
const current = props_and_spreads.at(-1);
const current_is_props = Array.isArray(current);
const props = current_is_props ? current : [];
props.push(prop);
if (!current_is_props) {
props_and_spreads.push(props);
function push_prop(prop, delay = false) {
const do_push = () => {
const current = props_and_spreads.at(-1);
const current_is_props = Array.isArray(current);
const props = current_is_props ? current : [];
props.push(prop);
if (!current_is_props) {
props_and_spreads.push(props);
}
};
if (delay) {
delayed_props.push(do_push);
} else {
do_push();
}
}
@ -176,38 +187,53 @@ export function build_component(node, component_name, context, anchor = context.
bind_this = attribute.expression;
} else {
if (dev) {
binding_initializers.push(
b.stmt(
b.call(
b.id('$.add_owner_effect'),
b.thunk(expression),
b.id(component_name),
is_ignored(node, 'ownership_invalid_binding') && b.true
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(
b.stmt(
b.call(
b.id('$.add_owner_effect'),
b.thunk(expression),
b.id(component_name),
is_ignored(node, 'ownership_invalid_binding') && b.true
)
)
)
);
);
}
}
const is_store_sub =
attribute.expression.type === 'Identifier' &&
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) {
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 {
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'));
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) {
context.state.init.push(...lets);
}

@ -39,6 +39,7 @@ import { SvelteSelf } from './visitors/SvelteSelf.js';
import { TitleElement } from './visitors/TitleElement.js';
import { UpdateExpression } from './visitors/UpdateExpression.js';
import { VariableDeclaration } from './visitors/VariableDeclaration.js';
import { SvelteBoundary } from './visitors/SvelteBoundary.js';
/** @type {Visitors} */
const global_visitors = {
@ -77,7 +78,8 @@ const template_visitors = {
SvelteHead,
SvelteHTML,
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)));
/** @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) {
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
fn.___snippet = true;
// TODO hoist where possible
context.state.init.push(fn);
if (node.metadata.can_hoist) {
context.state.hoisted.push(fn);
} else {
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) {
let tag = /** @type {Expression} */ (context.visit(node.tag));
if (tag.type !== 'Identifier') {
const tag_id = context.state.scope.generate('$$tag');
context.state.init.push(b.const(tag_id, tag));
tag = b.id(tag_id);
}
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') {
const tag_id = context.state.scope.generate('$$tag');
context.state.init.push(b.const(tag_id, tag));
tag = b.id(tag_id);
}
if (node.fragment.nodes.length > 0) {
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) {
/** @type {Array<Property[] | Expression>} */
const props_and_spreads = [];
/** @type {Array<() => void>} */
const delayed_props = [];
/** @type {Property[]} */
const custom_css_props = [];
@ -49,14 +51,23 @@ export function build_inline_component(node, expression, context) {
/**
* @param {Property} prop
* @param {boolean} [delay]
*/
function push_prop(prop) {
const current = props_and_spreads.at(-1);
const current_is_props = Array.isArray(current);
const props = current_is_props ? current : [];
props.push(prop);
if (!current_is_props) {
props_and_spreads.push(props);
function push_prop(prop, delay = false) {
const do_push = () => {
const current = props_and_spreads.at(-1);
const current_is_props = Array.isArray(current);
const props = current_is_props ? current : [];
props.push(prop);
if (!current_is_props) {
props_and_spreads.push(props);
}
};
if (delay) {
delayed_props.push(do_push);
} else {
do_push();
}
}
@ -81,11 +92,12 @@ export function build_inline_component(node, expression, context) {
const value = build_attribute_value(attribute.value, context, false, true);
push_prop(b.prop('init', b.key(attribute.name), value));
} 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(
b.get(attribute.name, [
b.return(/** @type {Expression} */ (context.visit(attribute.expression)))
])
]),
true
);
push_prop(
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))
])
]),
true
);
}
}
delayed_props.forEach((fn) => fn());
/** @type {Statement[]} */
const snippet_declarations = [];

@ -82,7 +82,8 @@ export function build_element_attributes(node, context) {
) {
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') {
class_index = attributes.length;
} else if (attribute.name === 'style') {

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

@ -527,31 +527,33 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
const scope = state.scope.child();
scopes.set(node, scope);
// declarations
for (const id of extract_identifiers(node.context)) {
const binding = scope.declare(id, 'each', 'const');
let inside_rest = false;
let is_rest_id = false;
walk(node.context, null, {
Identifier(node) {
if (inside_rest && node === id) {
is_rest_id = true;
if (node.context) {
// declarations
for (const id of extract_identifiers(node.context)) {
const binding = scope.declare(id, 'each', 'const');
let inside_rest = false;
let is_rest_id = false;
walk(node.context, null, {
Identifier(node) {
if (inside_rest && node === id) {
is_rest_id = true;
}
},
RestElement(_, { next }) {
const prev = inside_rest;
inside_rest = true;
next();
inside_rest = prev;
}
},
RestElement(_, { next }) {
const prev = inside_rest;
inside_rest = true;
next();
inside_rest = prev;
}
});
});
binding.metadata = { inside_rest: is_rest_id };
}
binding.metadata = { inside_rest: is_rest_id };
}
// Visit to pick up references from default initializers
visit(node.context, { scope });
// Visit to pick up references from default initializers
visit(node.context, { scope });
}
if (node.index) {
const is_keyed =

@ -1,5 +1,5 @@
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';
export interface Js {
@ -37,7 +37,7 @@ export interface ComponentAnalysis extends Analysis {
instance: Js;
template: Template;
/** Used for CSS pruning and scoping */
elements: Array<AST.RegularElement | AST.SvelteElement | AST.RenderTag>;
elements: Array<AST.RegularElement | AST.SvelteElement>;
runes: boolean;
exports: Array<{ name: string; alias: string | null }>;
/** Whether the component uses `$$props` */
@ -62,7 +62,6 @@ export interface ComponentAnalysis extends Analysis {
/** If `true`, should append styles through JavaScript */
inject_styles: boolean;
reactive_statements: Map<LabeledStatement, ReactiveStatement>;
top_level_snippets: VariableDeclaration[];
/** Identifiers that make up the `bind:group` expression -> internal group binding name */
binding_groups: Map<[key: string, bindings: Array<Binding | null>], Identifier>;
slot_names: Map<string, AST.SlotElement>;
@ -72,6 +71,18 @@ export interface ComponentAnalysis extends Analysis {
keyframes: 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' {

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

@ -166,6 +166,9 @@ export namespace AST {
dynamic: boolean;
args_with_call_expression: Set<number>;
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: {
scopes: Record<string, Scope>;
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 */
metadata: {
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';
}
export interface SvelteBoundary extends BaseElement {
type: 'SvelteBoundary';
name: 'svelte:boundary';
}
export interface SvelteHead extends BaseElement {
type: 'SvelteHead';
name: 'svelte:head';
@ -374,6 +390,10 @@ export namespace AST {
/** @internal */
metadata: {
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 {
type: 'EachBlock';
expression: Expression;
context: Pattern;
/** The `entry` in `{#each item as entry}`. `null` if `as` part is omitted */
context: Pattern | null;
body: Fragment;
fallback?: Fragment;
index?: string;
@ -443,6 +464,13 @@ export namespace AST {
expression: Identifier;
parameters: Pattern[];
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 {
@ -505,7 +533,8 @@ export type ElementLike =
| AST.SvelteHead
| AST.SvelteOptionsRaw
| AST.SvelteSelf
| AST.SvelteWindow;
| AST.SvelteWindow
| AST.SvelteBoundary;
export type TemplateNode =
| AST.Root

@ -102,7 +102,6 @@ export const codes = [
"perf_avoid_nested_class",
"reactive_declaration_invalid_placement",
"reactive_declaration_module_script_dependency",
"reactive_declaration_non_reactive_property",
"state_referenced_locally",
"store_rune_conflict",
"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");
}
/**
* 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?
* @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 {string} thing
* @param {string} parent
* @param {string} message
*/
export function node_invalid_placement_ssr(node, thing, parent) {
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`);
export function node_invalid_placement_ssr(node, message) {
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_html_changed',
'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.
* @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
* @returns {boolean}
* @param {string} [child_loc]
* @param {string} [ancestor_loc]
* @returns {string | null}
*/
export function is_tag_valid_with_ancestor(tag, ancestors) {
if (tag.includes('-')) return true; // custom elements can be anything
export function is_tag_valid_with_ancestor(child_tag, ancestors, child_loc, ancestor_loc) {
if (child_tag.includes('-')) return null; // custom elements can be anything
const target = ancestors[ancestors.length - 1];
const disallowed = disallowed_children[target];
if (!disallowed) return true;
const ancestor_tag = ancestors[ancestors.length - 1];
const disallowed = disallowed_children[ancestor_tag];
if (!disallowed) return null;
if ('reset_by' in disallowed && disallowed.reset_by) {
for (let i = ancestors.length - 2; i >= 0; 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
if (disallowed.reset_by.includes(ancestors[i])) {
return true;
return null;
}
}
}
return 'descendant' in disallowed ? !disallowed.descendant.includes(tag) : true;
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 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.
* @param {string} tag
* @param {string} child_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) {
if (tag.includes('-') || parent_tag?.includes('-')) return true; // custom elements can be anything
export function is_tag_valid_with_parent(child_tag, parent_tag, child_loc, parent_loc) {
if (child_tag.includes('-') || parent_tag?.includes('-')) return null; // custom elements can be anything
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 ('direct' in disallowed && disallowed.direct.includes(tag)) {
return false;
if ('direct' in disallowed && disallowed.direct.includes(child_tag)) {
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) {
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 'caption':
case 'col':
@ -196,18 +222,17 @@ export function is_tag_valid_with_parent(tag, parent_tag) {
case 'frame':
case 'head':
case 'html':
return `${child} cannot be a child of ${parent}`;
case 'thead':
case 'tbody':
case 'td':
case 'tfoot':
return `${child} must be the child of a \`<table>\`, not a ${parent}`;
case 'td':
case 'th':
case 'thead':
return `${child} must be the child of a \`<tr>\`, not a ${parent}`;
case 'tr':
// 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).
return false;
return `\`<tr>\` must be the child of a \`<thead>\`, \`<tbody>\`, or \`<tfoot>\`, not a ${parent}`;
}
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]
* @returns {EventDispatcher<EventMap>}
*/
@ -122,7 +122,7 @@ export function createEventDispatcher() {
*
* 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
* @returns {void}
*/
@ -145,7 +145,7 @@ export function beforeUpdate(fn) {
*
* 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
* @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
* 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.
*/
export class SvelteComponent<
@ -53,7 +53,7 @@ export class SvelteComponent<
/**
* @deprecated This constructor only exists when using the `asClassComponent` compatibility helper, which
* 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>>);
/**
@ -83,14 +83,14 @@ export class SvelteComponent<
/**
* @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.
*/
$destroy(): void;
/**
* @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.
*/
$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
* 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.
*/
$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
* 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.
*/
$on?(type: string, callback: (e: any) => void): () => void;
/**
* @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.
*/
$set?(props: Partial<Props>): void;

@ -4,21 +4,22 @@ export const RENDER_EFFECT = 1 << 3;
export const BLOCK_EFFECT = 1 << 4;
export const BRANCH_EFFECT = 1 << 5;
export const ROOT_EFFECT = 1 << 6;
export const UNOWNED = 1 << 7;
export const DISCONNECTED = 1 << 8;
export const CLEAN = 1 << 9;
export const DIRTY = 1 << 10;
export const MAYBE_DIRTY = 1 << 11;
export const INERT = 1 << 12;
export const DESTROYED = 1 << 13;
export const EFFECT_RAN = 1 << 14;
export const BOUNDARY_EFFECT = 1 << 7;
export const UNOWNED = 1 << 8;
export const DISCONNECTED = 1 << 9;
export const CLEAN = 1 << 10;
export const DIRTY = 1 << 11;
export const MAYBE_DIRTY = 1 << 12;
export const INERT = 1 << 13;
export const DESTROYED = 1 << 14;
export const EFFECT_RAN = 1 << 15;
/** '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 */
export const LEGACY_DERIVED_PROP = 1 << 16;
export const INSPECT_EFFECT = 1 << 17;
export const HEAD_EFFECT = 1 << 18;
export const EFFECT_HAS_DERIVED = 1 << 19;
export const LEGACY_DERIVED_PROP = 1 << 17;
export const INSPECT_EFFECT = 1 << 18;
export const HEAD_EFFECT = 1 << 19;
export const EFFECT_HAS_DERIVED = 1 << 20;
export const STATE_SYMBOL = Symbol('$state');
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_current_component_function } from '../../runtime.js';
import { get_first_child, get_next_sibling } from '../operations.js';
import { sanitize_location } from '../../dev/location.js';
/**
* @param {Element} element
@ -28,9 +29,7 @@ function check_hash(element, server_hash, value) {
location = `in ${dev_current_component_function[FILENAME]}`;
}
w.hydration_html_changed(
location?.replace(/\//g, '/\u200b') // prevent devtools trying to make it a clickable link by inserting a zero-width space
);
w.hydration_html_changed(sanitize_location(location));
}
/**

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

@ -60,13 +60,19 @@ export function remove_input_defaults(input) {
export function set_value(element, value) {
// @ts-expect-error
var attributes = (element.__attributes ??= {});
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
// `progress` elements always need their value set when its `0`
(element.value === value && (value !== 0 || element.nodeName !== 'PROGRESS'))
)
) {
return;
}
// @ts-expect-error
element.value = value;
}
@ -79,11 +85,60 @@ export function set_checked(element, checked) {
// @ts-expect-error
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
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 {string} attribute
@ -281,6 +336,9 @@ export function set_attributes(
element[`__${event_name}`] = value;
delegate([event_name]);
}
} else if (delegated) {
// @ts-ignore
element[`__${event_name}`] = undefined;
}
} else if (key === 'style' && value != null) {
element.style.cssText = value + '';

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

@ -80,15 +80,19 @@ export function init_select(select, get_value) {
export function bind_select_value(select, get, set = get) {
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} */
var value;
if (select.multiple) {
value = [].map.call(select.querySelectorAll(':checked'), get_option_value);
value = [].map.call(select.querySelectorAll(query), get_option_value);
} else {
/** @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);
}

@ -53,8 +53,8 @@ export function without_reactive_context(fn) {
* to notify all bindings when the form is reset
* @param {HTMLElement} element
* @param {string} event
* @param {() => void} handler
* @param {() => void} [on_reset]
* @param {(is_reset?: true) => void} handler
* @param {(is_reset?: true) => void} [on_reset]
*/
export function listen_to_event_and_reset_event(element, event, handler, on_reset = 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
element.__on_r = () => {
prev();
on_reset();
on_reset(true);
};
} else {
// @ts-expect-error
element.__on_r = on_reset;
element.__on_r = () => on_reset(true);
}
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 { active_effect } from '../runtime.js';
import { TEMPLATE_FRAGMENT, TEMPLATE_USE_IMPORT_NODE } from '../../../constants.js';
import { queue_micro_task } from './task.js';
/**
* @param {TemplateNode} start

@ -1,4 +1,5 @@
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 { add_locations } from './dev/elements.js';
export { hmr } from './dev/hmr.js';
@ -33,7 +34,10 @@ export {
set_xlink_attribute,
handle_lazy_img,
set_value,
set_checked
set_checked,
set_selected,
set_default_checked,
set_default_value
} from './dom/elements/attributes.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';
@ -124,6 +128,7 @@ export {
update_store,
mark_store_binding
} from './reactivity/store.js';
export { boundary } from './dom/blocks/boundary.js';
export { set_text } from './render.js';
export {
get,

@ -4,10 +4,13 @@ import { raf } from './timing.js';
// TODO move this into timing.js where it probably belongs
/**
* @param {number} now
* @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) => {
if (!task.c(now)) {
raf.tasks.delete(task);

@ -16,7 +16,8 @@ import {
set_is_flushing_effect,
set_signal_status,
untrack,
skip_reaction
skip_reaction,
capture_signals
} from '../runtime.js';
import {
DIRTY,
@ -39,10 +40,13 @@ import {
} from '../constants.js';
import { set } from './sources.js';
import * as e from '../errors.js';
import * as w from '../warnings.js';
import { DEV } from 'esm-env';
import { define_property } from '../../shared/utils.js';
import { get_next_sibling } from '../dom/operations.js';
import { destroy_derived } from './deriveds.js';
import { FILENAME } from '../../../constants.js';
import { get_location } from '../dev/location.js';
/**
* @param {'$effect' | '$effect.pre' | '$inspect'} rune
@ -261,14 +265,21 @@ export function effect(fn) {
* Internal representation of `$: ..`
* @param {() => any} deps
* @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);
/** @type {{ effect: null | Effect, ran: boolean }} */
var token = { effect: null, ran: false };
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(() => {
deps();
@ -278,7 +289,18 @@ export function legacy_pre_effect(deps, fn) {
token.ran = true;
set(context.l.r2, true);
untrack(fn);
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);
}
});
}
@ -464,12 +486,12 @@ export function destroy_effect(effect, remove_dom = true) {
}
// `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.prev =
effect.teardown =
effect.ctx =
effect.deps =
effect.parent =
effect.fn =
effect.nodes_start =
effect.nodes_end =

@ -24,7 +24,8 @@ import {
BLOCK_EFFECT,
ROOT_EFFECT,
LEGACY_DERIVED_PROP,
DISCONNECTED
DISCONNECTED,
BOUNDARY_EFFECT
} from './constants.js';
import { flush_tasks } from './dom/task.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_SYNC = 1;
// Used for DEV time error handling
/** @param {WeakSet<Error>} value */
const handled_errors = new WeakSet();
export let is_throwing_error = false;
// Used for controlling the flush of effects.
let scheduler_mode = FLUSH_MICROTASK;
// Used for handling scheduling
let is_micro_task_queued = false;
/** @type {Effect | null} */
let last_scheduled_effect = null;
export let is_flushing_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 | null} previous_effect
* @param {ComponentContext | null} component_context
*/
function handle_error(error, effect, component_context) {
// Given we don't yet have error boundaries, we will just always throw.
if (!DEV || handled_errors.has(error) || component_context === null) {
throw error;
export function handle_error(error, effect, previous_effect, component_context) {
if (is_throwing_error) {
if (previous_effect === null) {
is_throwing_error = false;
}
if (should_rethrow_error(effect)) {
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 effect_name = effect.fn?.name;
@ -268,6 +338,9 @@ function handle_error(error, effect, component_context) {
define_property(error, 'message', {
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;
@ -287,8 +360,11 @@ function handle_error(error, effect, component_context) {
});
}
handled_errors.add(error);
throw error;
propagate_error(error, effect);
if (should_rethrow_error(effect)) {
throw error;
}
}
/**
@ -449,7 +525,7 @@ export function update_effect(effect) {
dev_effect_stack.push(effect);
}
} catch (error) {
handle_error(/** @type {Error} */ (error), effect, previous_component_context);
handle_error(error, effect, previous_effect, previous_component_context || effect.ctx);
} finally {
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() {
if (flush_count > 1000) {
flush_count = 0;
if (DEV) {
try {
e.effect_update_depth_exceeded();
} catch (error) {
try {
e.effect_update_depth_exceeded();
} catch (error) {
if (DEV) {
// stack is garbage, ignore. Instead add a console.error message.
define_property(error, 'stack', {
value: ''
});
// eslint-disable-next-line no-console
console.error(
'Last ten effects were: ',
dev_effect_stack.slice(-10).map((d) => d.fn)
);
dev_effect_stack = [];
}
// Try and handle the error so it can be caught at a boundary, that's
// if there's an effect available from when it was last scheduled
if (last_scheduled_effect !== null) {
if (DEV) {
try {
handle_error(error, last_scheduled_effect, null, null);
} catch (e) {
// Only log the effect stack if the error is re-thrown
log_effect_stack();
throw e;
}
} else {
handle_error(error, last_scheduled_effect, null, null);
}
} else {
if (DEV) {
log_effect_stack();
}
throw error;
}
} else {
e.effect_update_depth_exceeded();
}
}
flush_count++;
@ -529,22 +625,28 @@ function flush_queued_effects(effects) {
for (var i = 0; i < length; i++) {
var effect = effects[i];
if ((effect.f & (DESTROYED | INERT)) === 0 && check_dirtiness(effect)) {
update_effect(effect);
// Effects with no dependencies or teardown do not get added to the effect tree.
// Deferred effects (e.g. `$effect(...)`) _are_ added to the tree because we
// don't know if we need to keep them until they are executed. Doing the check
// here (rather than in `update_effect`) allows us to skip the work for
// immediate effects.
if (effect.deps === null && effect.first === null && effect.nodes_start === null) {
if (effect.teardown === null) {
// remove this effect from the graph
unlink_effect(effect);
} else {
// keep the effect in the graph, but free up some memory
effect.fn = null;
if ((effect.f & (DESTROYED | INERT)) === 0) {
try {
if (check_dirtiness(effect)) {
update_effect(effect);
// Effects with no dependencies or teardown do not get added to the effect tree.
// Deferred effects (e.g. `$effect(...)`) _are_ added to the tree because we
// don't know if we need to keep them until they are executed. Doing the check
// here (rather than in `update_effect`) allows us to skip the work for
// immediate effects.
if (effect.deps === null && effect.first === null && effect.nodes_start === null) {
if (effect.teardown === null) {
// remove this effect from the graph
unlink_effect(effect);
} else {
// keep the effect in the graph, but free up some memory
effect.fn = null;
}
}
}
} catch (error) {
handle_error(error, effect, null, effect.ctx);
}
}
}
@ -558,8 +660,10 @@ function process_deferred() {
const previous_queued_root_effects = queued_root_effects;
queued_root_effects = [];
flush_queued_root_effects(previous_queued_root_effects);
if (!is_micro_task_queued) {
flush_count = 0;
last_scheduled_effect = null;
if (DEV) {
dev_effect_stack = [];
}
@ -578,6 +682,8 @@ export function schedule_effect(signal) {
}
}
last_scheduled_effect = signal;
var effect = signal;
while (effect.parent !== null) {
@ -612,13 +718,20 @@ function process_effects(effect, collected_effects) {
var flags = current_effect.f;
var is_branch = (flags & BRANCH_EFFECT) !== 0;
var is_skippable_branch = is_branch && (flags & CLEAN) !== 0;
var sibling = current_effect.next;
if (!is_skippable_branch && (flags & INERT) === 0) {
if ((flags & RENDER_EFFECT) !== 0) {
if (is_branch) {
current_effect.f ^= CLEAN;
} else if (check_dirtiness(current_effect)) {
update_effect(current_effect);
} else {
try {
if (check_dirtiness(current_effect)) {
update_effect(current_effect);
}
} catch (error) {
handle_error(error, current_effect, null, current_effect.ctx);
}
}
var child = current_effect.first;
@ -632,8 +745,6 @@ function process_effects(effect, collected_effects) {
}
}
var sibling = current_effect.next;
if (sibling === null) {
let parent = current_effect.parent;
@ -692,6 +803,7 @@ export function flush_sync(fn) {
}
flush_count = 0;
last_scheduled_effect = null;
if (DEV) {
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,
* then invalidates them.
* @param {() => any} fn
* Capture an array of all the signals that are read when `fn` is called
* @template T
* @param {() => T} fn
*/
export function invalidate_inner_signals(fn) {
export function capture_signals(fn) {
var previous_captured_signals = captured_signals;
captured_signals = new Set();
var captured = captured_signals;
var signal;
try {
untrack(fn);
if (previous_captured_signals !== null) {
@ -829,7 +943,19 @@ export function invalidate_inner_signals(fn) {
} finally {
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
if ((signal.f & LEGACY_DERIVED_PROP) !== 0) {
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]
* @returns {number}
* @returns {T}
*/
export function update(signal, d = 1) {
var value = +get(signal);
set(signal, value + d);
return value;
var value = get(signal);
var result = d === 1 ? value++ : 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]
* @returns {number}
* @returns {T}
*/
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 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
* @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
* @param {string} operator

@ -34,12 +34,11 @@ function stringify(element) {
/**
* @param {Payload} payload
* @param {Element} parent
* @param {Element} child
* @param {string} message
*/
function print_error(payload, parent, child) {
var message =
`node_invalid_placement_ssr: ${stringify(parent)} cannot contain ${stringify(child)}\n\n` +
function print_error(payload, message) {
message =
`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.';
if ((seen ??= new Set()).has(message)) return;
@ -72,15 +71,23 @@ export function push_element(payload, tag, line, column) {
var ancestor = parent.parent;
var ancestors = [parent.tag];
if (!is_tag_valid_with_parent(tag, parent.tag)) {
print_error(payload, parent, child);
}
const child_loc = filename ? `${filename}:${line}:${column}` : undefined;
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) {
ancestors.push(ancestor.tag);
if (!is_tag_valid_with_ancestor(tag, ancestors)) {
print_error(payload, ancestor, child);
}
const ancestor_loc = ancestor.filename
? `${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;
}
}

@ -231,11 +231,13 @@ export function spread_attributes(attrs, classes, styles, flags = 0) {
if (name[0] === '$' && name[1] === '$') continue; // faster than name.startsWith('$$')
if (INVALID_ATTR_NAME_CHAR_REGEX.test(name)) continue;
var value = attrs[name];
if (lowercase) {
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;

@ -1,2 +1,32 @@
import { MediaQuery } from 'svelte/reactivity';
export * from './spring.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 { DESTROYED } from '../internal/client/constants.js';
import { derived } from '../internal/client/index.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;
@ -12,6 +11,8 @@ export class SvelteDate extends Date {
/** @type {Map<keyof Date, Source<unknown>>} */
#deriveds = new Map();
#reaction = active_reaction;
/** @param {any[]} params */
constructor(...params) {
// @ts-ignore
@ -43,7 +44,12 @@ export class SvelteDate extends Date {
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(() => {
get(this.#time);
// @ts-ignore
@ -51,6 +57,8 @@ export class SvelteDate extends Date {
});
this.#deriveds.set(method, d);
set_active_reaction(reaction);
}
return get(d);

@ -642,3 +642,33 @@ test('Date methods invoked for the first time in a derived', () => {
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 { SvelteURL } from './url.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