pull/14290/head
Rich Harris 2 years ago
commit e69c992343

@ -23,8 +23,6 @@ Iterating over values can be done with an each block. The values in question can
</ul>
```
You can use each blocks to iterate over any array or array-like value — that is, any object with a `length` property.
An each block can also specify an _index_, equivalent to the second argument in an `array.map(...)` callback:
```svelte

@ -112,7 +112,7 @@ Snippets can reference themselves and each other ([demo](/playground/untitled#H4
## Passing snippets to components
Within the template, snippets are values just like any other. As such, they can be passed to components as props ([demo](/playground/untitled#H4sIAAAAAAAAE41SwY6bMBD9lRGplKQlYRMpF5ZF7T_0ttmDwSZYJbZrT9pGlv-9g4Fkk-xhxYV5vHlvhjc-aWQnXJK_-kSxo0jy5IcxSZrg2fSF-yM6FFQ7fbJ1jxSuttJguVd7lEejLcJPVnUCGquPMF9nsVoPjfNnohGx1sohMU4SHbzAa4_t0UNvmcOcGUNDzFP4jeccdikYK2v6sIWQ3lErpui5cDdPF_LmkVy3wlp5Vd5e2U_rHYSe_kYjFtl1KeVnTkljBEIrGBd2sYy8AtsyLlBk9DYhJHtTR_UbBDWybkR8NkqHWyOr_y74ZMNLz9f9AoG6ePkOJLMHLBp-xISvcPf11r0YUuMM2Ysfkgngh5XphUYKkJWU_FFz2UjBkxztSYT0cihR4LOn0tGaPrql439N-7Uh0Dl8MVYbt1jeJ1Fg7xDb_Uw2Y18YQqZ_S2U5FH1pS__dCkWMa3C0uR0pfQRTg89kE4bLLLDS_Dxy_Eywuo1TAnPAw4fqY1rvtH3W9w35ZZMgvU3jq8LhedwkguCHRhT_cMU6eVA5dKLB5wGutCWjlTOslupAxxrxceKoD2hzhe2qbmXHF1v1bbOcNCtW_zpYfVI8h5kQ4qY3mueHTlesW2C7TOEO4hcdwzgf3Nc7cZxUKKC4yuNhvIX_MlV_Xk0EAAA=)):
Within the template, snippets are values just like any other. As such, they can be passed to components as props ([demo](/playground/untitled#H4sIAAAAAAAAE3VS247aMBD9lZGpBGwDASRegonaPvQL2qdlH5zYEKvBNvbQLbL875VzAcKyj3PmzJnLGU8UOwqSkd8KJdaCk4TsZS0cyV49wYuJuQiQpGd-N2bu_ooaI1YwJ57hpVYoFDqSEepKKw3mO7VDeTTaIvxiRS1gb_URxvO0ibrS8WanIrHUyiHs7Vmigy28RmyHHmKvDMbMmFq4cQInvGSwTsBYWYoMVhCSB2rBFFPsyl0uruTlR3JZCWvlTXl1Yy_mawiR_rbZKZrellJ-5JQ0RiBUgnFhJ9OGR7HKmwVoilXeIye8DOJGfYCgRlZ3iE876TBsZPX7hPdteO75PC4QaIo8vwNPePmANQ2fMeEFHrLD7rR1jTNkW986E8C3KwfwVr8HSHOSEBT_kGRozyIkn_zQveXDL3rIfPJHtUDwzShJd_Qk3gQCbOGLsdq4yfTRJopRuin3I7nv6kL7ARRjmLdBDG3uv1mhuLA3V2mKtqNEf_oCn8p9aN-WYqH5peP4kWBl1UwJzAEPT9U7K--0fRrrWnPTXpCm1_EVdXjpNmlA8G1hPPyM1fKgMqjFHjctXGjLhZ05w0qpDhksGrybuNEHtJnCalZWsuaTlfq6nPaaBSv_HKw-K57BjzOiVj9ZKQYKzQjZodYFqydYTRN4gPhVzTDO2xnma3HsVWjaLjT8nbfwHy7Q5f2dBAAA)):
```svelte
<script>
@ -144,7 +144,7 @@ Within the template, snippets are values just like any other. As such, they can
Think about it like passing content instead of data to a component. The concept is similar to slots in web components.
As an authoring convenience, snippets declared directly _inside_ a component implicitly become props _on_ the component ([demo](/playground/untitled#H4sIAAAAAAAAE41Sy27bMBD8lYVcwHYrW4kBXxRFaP-htzgHSqQsojLJkuu2BqF_74qUrfhxCHQRh7MzO9z1SSM74ZL8zSeKHUSSJz-MSdIET2Y4uD-iQ0Fnp4-2HpDC1VYaLHdqh_JgtEX4yapOQGP1AebrLJzWsXD-QjQi1lo5JMZRooNXeBuwHXoYLHOYM2OoiXkKv_GUwzYFY2VNFxvo0xtqxRR9F-7z04X8fE-uW2GtnJQ3E_tpvYV-oL9Ti0U2hVJFjMMZslcfW-5DWj9zShojEFrBuLCLZR_9CmzLQCwy-psw8rxBgvkNhhpZd8F8NppE7Stbq_8u-GTKS8_XQ9Keqnl5BZP1AzTYP2bDV7i7_9hLEeda0iocNJeNFDzJ0R5Fn142JzA-uzsdBfLhldPxPdMhIPS0H1-M1cYtlnejwdBDfBXZjHXTFOg4BhuOtvTfrVDEmAZG2ew5ezYV-Ew2fVzVAivNTyPHzwSr29AlMAe8f6g-zuWDts-GusAmdBSkv3P7qnB4GpMEEHwsRPEPV6yTe5VDJxp8iXClLRmtnGG1VHva3oCPHQd9QJsrbFd1Kzu-2Khvz8uzZsXqX3urj4rnMBNCXNUG83zf6Yp1C2yXKdxA_KJjGOfRfb0Vh7MKDShEuV-M9_4_nq6svF4EAAA=)):
As an authoring convenience, snippets declared directly _inside_ a component implicitly become props _on_ the component ([demo](/playground/untitled#H4sIAAAAAAAAE3VSTa_aMBD8Kyu_SkAbCA-JSzBR20N_QXt6vIMTO8SqsY29tI2s_PcqTiB8vaPHs7MzuxuIZgdBMvJLo0QlOElIJZXwJHsLBBvb_XUASc7Mb9Yu_B-hsMMK5sUzvDQahUZPMkJ96aTFfKd3KA_WOISfrFACKmcOMFmk8TWUTjY73RFLoz1C5U4SPWzhrcN2GKDrlcGEWauEnyRwxCaDdQLWyVJksII2uaMWTDPNLtzX5YX8-kgua-GcHJVXI3u5WEPb0d83O03TMZSmfRzOkG1Db7mNacOL19JagVALxoWbztq-H8U6j0SaYp2P2BGbOyQ2v8PQIFMXLKRDk177pq0zf6d8bMrzwBdd0pamyPMb-IjNEzS2f86Gz_Dwf-2F9nvNSUJQ_EOSoTuJNvngqK5v4Pas7n4-OCwlEEJcQTIMO-nSQwtb-GSdsX46e9gbRoP9yGQ11I0rEuycunu6PHx1QnPhxm3SFN15MOlYEFJZtf0dUywMbwZOeBGsrKNLYB54-1R9WNqVdki7usim6VmQphf7mnpshiQRhNAXdoOfMyX3OgMlKtz0cGEcF27uLSul3mewjPjgOOoDukxjPS9rqfh0pb-8zs6aBSt_7505aZ7B9xOi0T9YKW4UooVsr0zB1BTrWQJ3EL-oWcZ572GxFoezCk37QLe3897-B2i2U62uBAAA)):
```svelte
<!-- this is semantically the same as the above -->
@ -165,7 +165,7 @@ As an authoring convenience, snippets declared directly _inside_ a component imp
</Table>
```
Any content inside the component tags that is _not_ a snippet declaration implicitly becomes part of the `children` snippet ([demo](/playground/untitled#H4sIAAAAAAAAE41S247aMBD9lVFYCegGsiDxks1G7T_0bdkHJ3aI1cR27aEtsvzvtZ0LZeGhiiJ5js-cmTMemzS8YybJ320iSM-SPPmmVJImeFEhML9Yh8zHRp51HZDC1JorLI_iiLxXUiN8J1XHoNGyh-U2i9F2SFy-epon1lIY9IwzRwNv8B6wI1oIJXNYEqV8E8sUfuIlh0MKSvPaX-zBpZ-oFRH-m7m7l5m8uyfXLdOaX5X3V_bL9gAu0D98i0V2NSWKwQ4lSN7s0LKLbgtsyxgXmT9NiBe-iaP-DYISSTcj4bcLI7hSDEHL3yu6dkPfBdLS0m1o3nk-LW9gX-gBGss9ZsMXuLu32VjZBdfRaelft5eUN5zRJEd9Zi6dlyEy_ncdOm_IxsGlULe8o5qJNFgE5x_9SWmpzGp9N2-MXQxz4c2cOQ-lZWQyF0Jd2q_-mjI9U1fr4FBPE8iuKTbjjRt2sMBK0svIsQtG6jb2CsQAdQ_1x9f5R9tmIS-yPToK-tNkQRQGL6ObCIIdEpH9wQ3p-Enk0LEGXwe4ktoX2hhFai5Ofi0jPnYc9QF1LrDdRK-rvXjerSfNitQ_TlqeBc1hwRi7yY3F81MnK9KtsF2n8Amis44ilA7VtwfWTyr-kaKV-_X4cH8BTOhfRzcEAAA=)):
Any content inside the component tags that is _not_ a snippet declaration implicitly becomes part of the `children` snippet ([demo](/playground/untitled#H4sIAAAAAAAAE3WOQQrCMBBFrzIMggql3ddY1Du4si5sOmIwnYRkFKX07lKqglqX8_7_w2uRDw1hjlsWI5ZqTPBoLEXMdy3K3fdZDzB5Ndfep_FKVnpWHSKNce1YiCVijirqYLwUJQOYxrsgsLmIOIZjcA1M02w4n-PpomSVvTclqyEutDX6DA2pZ7_ABIVugrmEC3XJH92P55_G39GodCmWBFrQJ2PrQAwdLGHig_NxNv9xrQa1dhWIawrv1Wzeqawa8953D-8QOmaEAQAA)):
```svelte
<!--- file: App.svelte --->

@ -12,10 +12,34 @@ The general syntax is `bind:property={expression}`, where `expression` is an _lv
<input bind:value />
```
Svelte creates an event listener that updates the bound value. If an element already has a listener for the same event, that listener will be fired before the bound value is updated.
Most bindings are _two-way_, meaning that changes to the value will affect the element and vice versa. A few bindings are _readonly_, meaning that changing their value will have no effect on the element.
## Function bindings
You can also use `bind:property={get, set}`, where `get` and `set` are functions, allowing you to perform validation and transformation:
```svelte
<input bind:value={
() => value,
(v) => value = v.toLowerCase()}
/>
```
In the case of readonly bindings like [dimension bindings](#Dimensions), the `get` value should be `null`:
```svelte
<div
bind:clientWidth={null, redraw}
bind:clientHeight={null, redraw}
>...</div>
```
> [!NOTE]
> Function bindings are available in Svelte 5.9.0 and newer.
## `<input bind:value>`
A `bind:value` directive on an `<input>` element binds the input's `value` property:

@ -49,7 +49,7 @@ export const userState = $state({
```svelte
<!--- file: App.svelte --->
<script>
import { userState } from './state.svelte';
import { userState } from './state.svelte.js';
</script>
<p>User name: {userState.name}</p>

@ -22,7 +22,7 @@ export const myGlobalState = $state({
```svelte
<!--- file: App.svelte --->
<script>
import { myGlobalState } from './state.svelte';
import { myGlobalState } from './state.svelte.js';
// ...
</script>
```

@ -33,19 +33,22 @@ Note that unlike calling `new App(...)` in Svelte 4, things like effects (includ
## `unmount`
Unmounts a component created with [`mount`](#mount) or [`hydrate`](#hydrate):
Unmounts a component that was previously created with [`mount`](#mount) or [`hydrate`](#hydrate).
If `options.outro` is `true`, [transitions](transition) will play before the component is removed from the DOM:
```js
// @errors: 1109
import { mount, unmount } from 'svelte';
import App from './App.svelte';
const app = mount(App, {...});
const app = mount(App, { target: document.body });
// later
unmount(app);
unmount(app, { outro: true });
```
Returns a `Promise` that resolves after transitions have completed if `options.outro` is true, or immediately otherwise.
## `render`
Only available on the server and when compiling with the `server` option. Takes a component and returns an object with `body` and `head` properties on it, which you can use to populate the HTML when server-rendering your app:

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

@ -66,6 +66,31 @@ The easiest way to log a value as it changes over time is to use the [`$inspect`
The `%attribute%` attribute on `%html%` changed its value between server and client renders. The client value, `%value%`, will be ignored in favour of the server value
```
Certain attributes like `src` on an `<img>` element will not be repaired during hydration, i.e. the server value will be kept. That's because updating these attributes can cause the image to be refetched (or in the case of an `<iframe>`, for the frame to be reloaded), even if they resolve to the same resource.
To fix this, either silence the warning with a [`svelte-ignore`](basic-markup#Comments) comment, or ensure that the value stays the same between server and client. If you really need the value to change on hydration, you can force an update like this:
```svelte
<script>
let { src } = $props();
if (typeof window !== 'undefined') {
// stash the value...
const initial = src;
// unset it...
src = undefined;
$effect(() => {
// ...and reset after we've mounted
src = initial;
});
}
</script>
<img {src} />
```
### hydration_html_changed
```
@ -76,6 +101,31 @@ The value of an `{@html ...}` block changed between server and client renders. T
The value of an `{@html ...}` block %location% changed between server and client renders. The client value will be ignored in favour of the server value
```
If the `{@html ...}` value changes between the server and the client, it will not be repaired during hydration, i.e. the server value will be kept. That's because change detection during hydration is expensive and usually unnecessary.
To fix this, either silence the warning with a [`svelte-ignore`](basic-markup#Comments) comment, or ensure that the value stays the same between server and client. If you really need the value to change on hydration, you can force an update like this:
```svelte
<script>
let { markup } = $props();
if (typeof window !== 'undefined') {
// stash the value...
const initial = markup;
// unset it...
markup = undefined;
$effect(() => {
// ...and reset after we've mounted
markup = initial;
});
}
</script>
{@html markup}
```
### hydration_mismatch
```
@ -86,6 +136,10 @@ Hydration failed because the initial UI does not match what was rendered on the
Hydration failed because the initial UI does not match what was rendered on the server. The error occurred near %location%
```
This warning is thrown when Svelte encounters an error while hydrating the HTML from the server. During hydration, Svelte walks the DOM, expecting a certain structure. If that structure is different (for example because the HTML was repaired by the DOM because of invalid HTML), then Svelte will run into issues, resulting in this warning.
During development, this error is often preceeded by a `console.error` detailing the offending HTML, which needs fixing.
### invalid_raw_snippet_render
```
@ -110,6 +164,10 @@ Tried to unmount a component that was not mounted
%parent% passed a value to %child% with `bind:`, but the value is owned by %owner%. Consider creating a binding between %owner% and %parent%
```
Consider three components `GrandParent`, `Parent` and `Child`. If you do `<GrandParent bind:value>`, inside `GrandParent` pass on the variable via `<Parent {value} />` (note the missing `bind:`) and then do `<Child bind:value>` inside `Parent`, this warning is thrown.
To fix it, `bind:` to the value instead of just passing a property (i.e. in this example do `<Parent bind:value />`).
### ownership_invalid_mutation
```
@ -120,45 +178,31 @@ 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_.
Consider the following code:
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;
```
```svelte
<!--- file: App.svelte --->
<script>
import Child from './Child.svelte';
let person = $state({ name: 'Florida', surname: 'Man' });
</script>
```js
let a = 1, b = 2;
// ---cut---
const sum = $derived(a + b);
<Child {person} />
```
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;
```svelte
<!--- file: Child.svelte --->
<script>
let { person } = $props();
</script>
// the compiler can't 'see' that `sum` depends on `a` and `b`, but
// they _would_ be read while executing the `$derived` version
$: sum = add();
<input bind:value={person.name}>
<input bind:value={person.surname}>
```
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.
`Child` is mutating `person` which is owned by `App` without being explicitly "allowed" to do so. This is strongly discouraged since it can create code that is hard to reason about at scale ("who mutated this value?"), hence the warning.
When you [migrate this component](https://svelte.dev/docs/svelte/v5-migration-guide) to runes mode, the behaviour will change accordingly.
To fix it, either create callback props to communicate changes, or mark `person` as [`$bindable`]($bindable).
### state_proxy_equality_mismatch

@ -78,10 +78,16 @@ Sequence expressions are not allowed as attribute/directive values in runes mode
Attribute values containing `{...}` must be enclosed in quote marks, unless the value only contains the expression
```
### bind_group_invalid_expression
```
`bind:group` can only bind to an Identifier or MemberExpression
```
### bind_invalid_expression
```
Can only bind to an Identifier or MemberExpression
Can only bind to an Identifier or MemberExpression or a `{get, set}` pair
```
### bind_invalid_name
@ -94,6 +100,12 @@ Can only bind to an Identifier or MemberExpression
`bind:%name%` is not a valid binding. %explanation%
```
### bind_invalid_parens
```
`bind:%name%={get, set}` must not have surrounding parentheses
```
### bind_invalid_target
```

@ -610,6 +610,20 @@ Empty block
Unused CSS selector "%name%"
```
Svelte traverses both the template and the `<style>` tag to find out which of the CSS selectors are not used within the template, so it can remove them.
In some situations a selector may target an element that is not 'visible' to the compiler, for example because it is part of an `{@html ...}` tag or you're overriding styles in a child component. In these cases, use [`:global`](/docs/svelte/global-styles) to preserve the selector as-is:
```svelte
<div class="post">{@html content}</div>
<style>
.post :global {
p {...}
}
</style>
```
### element_invalid_self_closing_tag
```
@ -622,6 +636,8 @@ Self-closing HTML tags for non-void elements are ambiguous — use `<%name% ...>
Using `on:%name%` to listen to the %name% event is deprecated. Use the event attribute `on%name%` instead
```
See [the migration guide](v5-migration-guide#Event-changes) for more info.
### export_let_unused
```
@ -640,6 +656,8 @@ Component has unused export property '%name%'. If it is for external reference o
Svelte 5 components are no longer classes. Instantiate them using `mount` or `hydrate` (imported from 'svelte') instead.
```
See the [migration guide](v5-migration-guide#Components-are-no-longer-classes) for more info.
### node_invalid_placement_ssr
```
@ -660,6 +678,30 @@ This code will work when the component is rendered on the client (which is why t
`%name%` is updated, but is not declared with `$state(...)`. Changing its value will not correctly trigger updates
```
This warning is thrown when the compiler detects the following:
- a variable was declared without `$state` or `$state.raw`
- the variable is reassigned
- the variable is read in a reactive context
In this case, changing the value will not correctly trigger updates. Example:
```svelte
<script>
let reactive = $state('reactive');
let stale = 'stale';
</script>
<p>This value updates: {reactive}</p>
<p>This value does not update: {stale}</p>
<button onclick={() => {
stale = 'updated';
reactive = 'updated';
}}>update</button>
```
To fix this, wrap your variable declaration with `$state`.
### options_deprecated_accessors
```
@ -732,6 +774,12 @@ Reassignments of module-level declarations will not cause reactive statements to
`context="module"` is deprecated, use the `module` attribute instead
```
```svelte
<script ---context="module"--- +++module+++>
let foo = 'bar';
</script>
```
### script_unknown_attribute
```
@ -744,12 +792,79 @@ Unrecognized attribute — should be one of `generics`, `lang` or `module`. If t
Using `<slot>` to render parent content is deprecated. Use `{@render ...}` tags instead
```
See [the migration guide](v5-migration-guide#Snippets-instead-of-slots) for more info.
### state_referenced_locally
```
State referenced in its own scope will never update. Did you mean to reference it inside a closure?
```
This warning is thrown when the compiler detects the following:
- A reactive variable is declared
- the variable is reassigned
- the variable is referenced inside the same scope it is declared and it is a non-reactive context
In this case, the state reassignment will not be noticed by whatever you passed it to. For example, if you pass the state to a function, that function will not notice the updates:
```svelte
<!--- file: Parent.svelte --->
<script>
import { setContext } from 'svelte';
let count = $state(0);
// warning: state_referenced_locally
setContext('count', count);
</script>
<button onclick={() => count++}>
increment
</button>
```
```svelte
<!--- file: Child.svelte --->
<script>
import { getContext } from 'svelte';
const count = getContext('count');
</script>
<!-- This will never update -->
<p>The count is {count}</p>
```
To fix this, reference the variable such that it is lazily evaluated. For the above example, this can be achieved by wrapping `count` in a function:
```svelte
<!--- file: Parent.svelte --->
<script>
import { setContext } from 'svelte';
let count = $state(0);
setContext('count', +++() => count+++);
</script>
<button onclick={() => count++}>
increment
</button>
```
```svelte
<!--- file: Child.svelte --->
<script>
import { getContext } from 'svelte';
const count = getContext('count');
</script>
<!-- This will update -->
<p>The count is {+++count()+++}</p>
```
For more info, see [Passing state into functions]($state#Passing-state-into-functions).
### store_rune_conflict
```
@ -805,6 +920,8 @@ A derived value may be used in other contexts:
`<svelte:self>` is deprecated — use self-imports (e.g. `import %name% from './%basename%'`) instead
```
See [the note in the docs](legacy-svelte-self) for more info.
### unknown_code
```

@ -5,3 +5,5 @@
```
`%name%(...)` is not available on the server
```
Certain methods such as `mount` cannot be invoked while running in a server context. Avoid calling them eagerly, i.e. not during render.

@ -6,12 +6,54 @@
Cannot use `{@render children(...)}` if the parent component uses `let:` directives. Consider using a named snippet instead
```
This error would be thrown in a setup like this:
```svelte
<!--- file: Parent.svelte --->
<List {items} let:entry>
<span>{entry}</span>
</List>
```
```svelte
<!--- file: List.svelte --->
<script>
let { items, children } = $props();
</script>
<ul>
{#each items as item}
<li>{@render children(item)}</li>
{/each}
</ul>
```
Here, `List.svelte` is using `{@render children(item)` which means it expects `Parent.svelte` to use snippets. Instead, `Parent.svelte` uses the deprecated `let:` directive. This combination of APIs is incompatible, hence the error.
### lifecycle_outside_component
```
`%name%(...)` can only be used during component initialisation
```
Certain lifecycle methods can only be used during component initialisation. To fix this, make sure you're invoking the method inside the _top level of the instance script_ of your component.
```svelte
<script>
import { onMount } from 'svelte';
function handleClick() {
// This is wrong
onMount(() => {})
}
// This is correct
onMount(() => {})
</script>
<button onclick={handleClick}>click me</button>
```
### store_invalid_shape
```

@ -6,6 +6,8 @@
`<svelte:element this="%tag%">` is a void element — it cannot have content
```
Elements such as `<input>` cannot have content, any children passed to these elements will be ignored.
### state_snapshot_uncloneable
```
@ -17,3 +19,10 @@ The following properties cannot be cloned with `$state.snapshot` — the return
%properties%
```
`$state.snapshot` tries to clone the given value in order to return a reference that no longer changes. Certain objects may not be cloneable, in which case the original value is returned. In the following example, `property` is cloned, but `window` is not, because DOM elements are uncloneable:
```js
const object = $state({ property: 'this is cloneable', window })
const snapshot = $state.snapshot(object);
```

@ -0,0 +1,15 @@
---
title: svelte/reactivity/window
---
This module exports reactive versions of various `window` values, each of which has a reactive `current` property that you can reference in reactive contexts (templates, [deriveds]($derived) and [effects]($effect)) without using [`<svelte:window>`](svelte-window) bindings or manually creating your own event listeners.
```svelte
<script>
import { innerWidth, innerHeight } from 'svelte/reactivity/window';
</script>
<p>{innerWidth.current}x{innerHeight.current}</p>
```
> MODULE: svelte/reactivity/window

@ -1,5 +1,135 @@
# svelte
## 5.13.0
### Minor Changes
- feat: add `outro` option to `unmount` ([#14540](https://github.com/sveltejs/svelte/pull/14540))
- feat: provide loose parser mode ([#14691](https://github.com/sveltejs/svelte/pull/14691))
## 5.12.0
### Minor Changes
- feat: expose more AST types from `"svelte/compiler"` ([#14601](https://github.com/sveltejs/svelte/pull/14601))
### Patch Changes
- fix: don't add parenthesis to media query if already present ([#14699](https://github.com/sveltejs/svelte/pull/14699))
- fix: ensure if block paths retain correct template namespacing ([#14685](https://github.com/sveltejs/svelte/pull/14685))
## 5.11.3
### Patch Changes
- fix: allow unquoted slash in attributes ([#14615](https://github.com/sveltejs/svelte/pull/14615))
- fix: better handle hydration of script/style elements ([#14683](https://github.com/sveltejs/svelte/pull/14683))
- fix: make `defaultValue` work with spread ([#14640](https://github.com/sveltejs/svelte/pull/14640))
- fix: avoid mutation validation for invalidate_inner_signals ([#14688](https://github.com/sveltejs/svelte/pull/14688))
## 5.11.2
### Patch Changes
- fix: correctly handle ssr for `reactivity/window` ([#14681](https://github.com/sveltejs/svelte/pull/14681))
## 5.11.1
### Patch Changes
- fix: account for global block in `is_empty` ([#14677](https://github.com/sveltejs/svelte/pull/14677))
- fix: remove overzealous `reactive_declaration_non_reactive_property` warning ([#14663](https://github.com/sveltejs/svelte/pull/14663))
## 5.11.0
### Minor Changes
- feat: add `svelte/reactivity/window` module ([#14660](https://github.com/sveltejs/svelte/pull/14660))
### Patch Changes
- fix: take into account registration state when setting custom element props ([#14508](https://github.com/sveltejs/svelte/pull/14508))
## 5.10.1
### Patch Changes
- fix: ensure snippet hoisting works in the correct scope ([#14642](https://github.com/sveltejs/svelte/pull/14642))
- fix: ensure $state.snapshot clones holey arrays correctly ([#14657](https://github.com/sveltejs/svelte/pull/14657))
- fix: restore input binding selection position ([#14649](https://github.com/sveltejs/svelte/pull/14649))
- fix: transform everything that is not a selector inside `:global` ([#14577](https://github.com/sveltejs/svelte/pull/14577))
- Overwrite Spring.#last_value when using .set() with {instant: true} ([#14656](https://github.com/sveltejs/svelte/pull/14656))
- fix: don't emit assignment warnings for bindings ([#14651](https://github.com/sveltejs/svelte/pull/14651))
## 5.10.0
### Minor Changes
- feat: provide links to documentation for errors/warnings ([#14629](https://github.com/sveltejs/svelte/pull/14629))
### Patch Changes
- fix: allow exports with source from script module even if no bind is present ([#14620](https://github.com/sveltejs/svelte/pull/14620))
- fix: deconflict `get_name` for literal class properties ([#14607](https://github.com/sveltejs/svelte/pull/14607))
## 5.9.1
### Patch Changes
- fix: mark subtree dynamic for bind with sequence expressions ([#14626](https://github.com/sveltejs/svelte/pull/14626))
## 5.9.0
### Minor Changes
- feat: add support for bind getters/setters ([#14307](https://github.com/sveltejs/svelte/pull/14307))
### Patch Changes
- fix: always run `if` block code the first time ([#14597](https://github.com/sveltejs/svelte/pull/14597))
## 5.8.1
### Patch Changes
- fix: reinstate missing prefersReducedMotion export ([#14586](https://github.com/sveltejs/svelte/pull/14586))
## 5.8.0
### Minor Changes
- feat: add `Spring` and `Tween` classes to `svelte/motion` ([#11519](https://github.com/sveltejs/svelte/pull/11519))
## 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

@ -52,18 +52,72 @@ The easiest way to log a value as it changes over time is to use the [`$inspect`
> The `%attribute%` attribute on `%html%` changed its value between server and client renders. The client value, `%value%`, will be ignored in favour of the server value
Certain attributes like `src` on an `<img>` element will not be repaired during hydration, i.e. the server value will be kept. That's because updating these attributes can cause the image to be refetched (or in the case of an `<iframe>`, for the frame to be reloaded), even if they resolve to the same resource.
To fix this, either silence the warning with a [`svelte-ignore`](basic-markup#Comments) comment, or ensure that the value stays the same between server and client. If you really need the value to change on hydration, you can force an update like this:
```svelte
<script>
let { src } = $props();
if (typeof window !== 'undefined') {
// stash the value...
const initial = src;
// unset it...
src = undefined;
$effect(() => {
// ...and reset after we've mounted
src = initial;
});
}
</script>
<img {src} />
```
## hydration_html_changed
> The value of an `{@html ...}` block changed between server and client renders. The client value will be ignored in favour of the server value
> The value of an `{@html ...}` block %location% changed between server and client renders. The client value will be ignored in favour of the server value
If the `{@html ...}` value changes between the server and the client, it will not be repaired during hydration, i.e. the server value will be kept. That's because change detection during hydration is expensive and usually unnecessary.
To fix this, either silence the warning with a [`svelte-ignore`](basic-markup#Comments) comment, or ensure that the value stays the same between server and client. If you really need the value to change on hydration, you can force an update like this:
```svelte
<script>
let { markup } = $props();
if (typeof window !== 'undefined') {
// stash the value...
const initial = markup;
// unset it...
markup = undefined;
$effect(() => {
// ...and reset after we've mounted
markup = initial;
});
}
</script>
{@html markup}
```
## hydration_mismatch
> Hydration failed because the initial UI does not match what was rendered on the server
> Hydration failed because the initial UI does not match what was rendered on the server. The error occurred near %location%
This warning is thrown when Svelte encounters an error while hydrating the HTML from the server. During hydration, Svelte walks the DOM, expecting a certain structure. If that structure is different (for example because the HTML was repaired by the DOM because of invalid HTML), then Svelte will run into issues, resulting in this warning.
During development, this error is often preceeded by a `console.error` detailing the offending HTML, which needs fixing.
## invalid_raw_snippet_render
> The `render` function passed to `createRawSnippet` should return HTML for a single element
@ -80,49 +134,41 @@ The easiest way to log a value as it changes over time is to use the [`$inspect`
> %parent% passed a value to %child% with `bind:`, but the value is owned by %owner%. Consider creating a binding between %owner% and %parent%
Consider three components `GrandParent`, `Parent` and `Child`. If you do `<GrandParent bind:value>`, inside `GrandParent` pass on the variable via `<Parent {value} />` (note the missing `bind:`) and then do `<Child bind:value>` inside `Parent`, this warning is thrown.
To fix it, `bind:` to the value instead of just passing a property (i.e. in this example do `<Parent bind:value />`).
## ownership_invalid_mutation
> Mutating a value outside the component that created it is strongly discouraged. Consider passing values to child components with `bind:`, or use a callback instead
> %component% mutated a value owned by %owner%. This is strongly discouraged. Consider passing values to child components with `bind:`, or use a callback instead
## 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
Consider the following code:
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;
```
```svelte
<!--- file: App.svelte --->
<script>
import Child from './Child.svelte';
let person = $state({ name: 'Florida', surname: 'Man' });
</script>
```js
let a = 1, b = 2;
// ---cut---
const sum = $derived(a + b);
<Child {person} />
```
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;
```svelte
<!--- file: Child.svelte --->
<script>
let { person } = $props();
</script>
// the compiler can't 'see' that `sum` depends on `a` and `b`, but
// they _would_ be read while executing the `$derived` version
$: sum = add();
<input bind:value={person.name}>
<input bind:value={person.surname}>
```
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.
`Child` is mutating `person` which is owned by `App` without being explicitly "allowed" to do so. This is strongly discouraged since it can create code that is hard to reason about at scale ("who mutated this value?"), hence the warning.
When you [migrate this component](https://svelte.dev/docs/svelte/v5-migration-guide) to runes mode, the behaviour will change accordingly.
To fix it, either create callback props to communicate changes, or mark `person` as [`$bindable`]($bindable).
## state_proxy_equality_mismatch

@ -50,9 +50,13 @@
> Attribute values containing `{...}` must be enclosed in quote marks, unless the value only contains the expression
## bind_group_invalid_expression
> `bind:group` can only bind to an Identifier or MemberExpression
## bind_invalid_expression
> Can only bind to an Identifier or MemberExpression
> Can only bind to an Identifier or MemberExpression or a `{get, set}` pair
## bind_invalid_name
@ -60,6 +64,10 @@
> `bind:%name%` is not a valid binding. %explanation%
## bind_invalid_parens
> `bind:%name%={get, set}` must not have surrounding parentheses
## bind_invalid_target
> `bind:%name%` can only be used with %elements%

@ -6,10 +6,36 @@
> Svelte 5 components are no longer classes. Instantiate them using `mount` or `hydrate` (imported from 'svelte') instead.
See the [migration guide](v5-migration-guide#Components-are-no-longer-classes) for more info.
## non_reactive_update
> `%name%` is updated, but is not declared with `$state(...)`. Changing its value will not correctly trigger updates
This warning is thrown when the compiler detects the following:
- a variable was declared without `$state` or `$state.raw`
- the variable is reassigned
- the variable is read in a reactive context
In this case, changing the value will not correctly trigger updates. Example:
```svelte
<script>
let reactive = $state('reactive');
let stale = 'stale';
</script>
<p>This value updates: {reactive}</p>
<p>This value does not update: {stale}</p>
<button onclick={() => {
stale = 'updated';
reactive = 'updated';
}}>update</button>
```
To fix this, wrap your variable declaration with `$state`.
## perf_avoid_inline_class
> Avoid 'new class' — instead, declare the class at the top level scope
@ -30,6 +56,71 @@
> State referenced in its own scope will never update. Did you mean to reference it inside a closure?
This warning is thrown when the compiler detects the following:
- A reactive variable is declared
- the variable is reassigned
- the variable is referenced inside the same scope it is declared and it is a non-reactive context
In this case, the state reassignment will not be noticed by whatever you passed it to. For example, if you pass the state to a function, that function will not notice the updates:
```svelte
<!--- file: Parent.svelte --->
<script>
import { setContext } from 'svelte';
let count = $state(0);
// warning: state_referenced_locally
setContext('count', count);
</script>
<button onclick={() => count++}>
increment
</button>
```
```svelte
<!--- file: Child.svelte --->
<script>
import { getContext } from 'svelte';
const count = getContext('count');
</script>
<!-- This will never update -->
<p>The count is {count}</p>
```
To fix this, reference the variable such that it is lazily evaluated. For the above example, this can be achieved by wrapping `count` in a function:
```svelte
<!--- file: Parent.svelte --->
<script>
import { setContext } from 'svelte';
let count = $state(0);
setContext('count', +++() => count+++);
</script>
<button onclick={() => count++}>
increment
</button>
```
```svelte
<!--- file: Child.svelte --->
<script>
import { getContext } from 'svelte';
const count = getContext('count');
</script>
<!-- This will update -->
<p>The count is {+++count()+++}</p>
```
For more info, see [Passing state into functions]($state#Passing-state-into-functions).
## store_rune_conflict
> It looks like you're using the `$%name%` rune, but there is a local binding called `%name%`. Referencing a local variable with a `$` prefix will create a store subscription. Please rename `%name%` to avoid the ambiguity

@ -1,3 +1,17 @@
## css_unused_selector
> Unused CSS selector "%name%"
Svelte traverses both the template and the `<style>` tag to find out which of the CSS selectors are not used within the template, so it can remove them.
In some situations a selector may target an element that is not 'visible' to the compiler, for example because it is part of an `{@html ...}` tag or you're overriding styles in a child component. In these cases, use [`:global`](/docs/svelte/global-styles) to preserve the selector as-is:
```svelte
<div class="post">{@html content}</div>
<style>
.post :global {
p {...}
}
</style>
```

@ -38,6 +38,8 @@
> Using `on:%name%` to listen to the %name% event is deprecated. Use the event attribute `on%name%` instead
See [the migration guide](v5-migration-guide#Event-changes) for more info.
## 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
@ -54,6 +56,12 @@ This code will work when the component is rendered on the client (which is why t
> `context="module"` is deprecated, use the `module` attribute instead
```svelte
<script ---context="module"--- +++module+++>
let foo = 'bar';
</script>
```
## script_unknown_attribute
> Unrecognized attribute — should be one of `generics`, `lang` or `module`. If this exists for a preprocessor, ensure that the preprocessor removes it
@ -62,6 +70,8 @@ This code will work when the component is rendered on the client (which is why t
> Using `<slot>` to render parent content is deprecated. Use `{@render ...}` tags instead
See [the migration guide](v5-migration-guide#Snippets-instead-of-slots) for more info.
## svelte_component_deprecated
> `<svelte:component>` is deprecated in runes mode — components are dynamic by default
@ -104,3 +114,5 @@ A derived value may be used in other contexts:
## svelte_self_deprecated
> `<svelte:self>` is deprecated — use self-imports (e.g. `import %name% from './%basename%'`) instead
See [the note in the docs](legacy-svelte-self) for more info.

@ -1,3 +1,5 @@
## lifecycle_function_unavailable
> `%name%(...)` is not available on the server
Certain methods such as `mount` cannot be invoked while running in a server context. Avoid calling them eagerly, i.e. not during render.

@ -2,10 +2,52 @@
> Cannot use `{@render children(...)}` if the parent component uses `let:` directives. Consider using a named snippet instead
This error would be thrown in a setup like this:
```svelte
<!--- file: Parent.svelte --->
<List {items} let:entry>
<span>{entry}</span>
</List>
```
```svelte
<!--- file: List.svelte --->
<script>
let { items, children } = $props();
</script>
<ul>
{#each items as item}
<li>{@render children(item)}</li>
{/each}
</ul>
```
Here, `List.svelte` is using `{@render children(item)` which means it expects `Parent.svelte` to use snippets. Instead, `Parent.svelte` uses the deprecated `let:` directive. This combination of APIs is incompatible, hence the error.
## lifecycle_outside_component
> `%name%(...)` can only be used during component initialisation
Certain lifecycle methods can only be used during component initialisation. To fix this, make sure you're invoking the method inside the _top level of the instance script_ of your component.
```svelte
<script>
import { onMount } from 'svelte';
function handleClick() {
// This is wrong
onMount(() => {})
}
// This is correct
onMount(() => {})
</script>
<button onclick={handleClick}>click me</button>
```
## store_invalid_shape
> `%name%` is not a store with a `subscribe` method

@ -2,6 +2,8 @@
> `<svelte:element this="%tag%">` is a void element — it cannot have content
Elements such as `<input>` cannot have content, any children passed to these elements will be ignored.
## state_snapshot_uncloneable
> Value cannot be cloned with `$state.snapshot` — the original value was returned
@ -9,3 +11,10 @@
> The following properties cannot be cloned with `$state.snapshot` — the return value contains the originals:
>
> %properties%
`$state.snapshot` tries to clone the given value in order to return a reference that no longer changes. Certain objects may not be cloneable, in which case the original value is returned. In the following example, `property` is cloned, but `window` is not, because DOM elements are uncloneable:
```js
const object = $state({ property: 'this is cloneable', window })
const snapshot = $state.snapshot(object);
```

@ -2,7 +2,7 @@
"name": "svelte",
"description": "Cybernetically enhanced web apps",
"license": "MIT",
"version": "5.6.2",
"version": "5.13.0",
"type": "module",
"types": "./types/index.d.ts",
"engines": {
@ -73,6 +73,10 @@
"browser": "./src/reactivity/index-client.js",
"default": "./src/reactivity/index-server.js"
},
"./reactivity/window": {
"types": "./types/index.d.ts",
"default": "./src/reactivity/window/index.js"
},
"./server": {
"types": "./types/index.d.ts",
"default": "./src/server/index.js"

@ -35,6 +35,7 @@ await createBundle({
[`${pkg.name}/legacy`]: `${dir}/src/legacy/legacy-client.js`,
[`${pkg.name}/motion`]: `${dir}/src/motion/public.d.ts`,
[`${pkg.name}/reactivity`]: `${dir}/src/reactivity/index-client.js`,
[`${pkg.name}/reactivity/window`]: `${dir}/src/reactivity/window/index.js`,
[`${pkg.name}/server`]: `${dir}/src/server/index.d.ts`,
[`${pkg.name}/store`]: `${dir}/src/store/public.d.ts`,
[`${pkg.name}/transition`]: `${dir}/src/transition/public.d.ts`,

@ -339,7 +339,7 @@ function transform(name, dest) {
};
for (let i = 0; i < node.expressions.length; i += 1) {
const q = node.quasis[i + 1];
const q = structuredClone(node.quasis[i + 1]);
const e = node.expressions[i];
if (e.type === 'Literal' && e.value === 'CODE') {
@ -355,10 +355,12 @@ function transform(name, dest) {
}
if (message.type === 'TemplateLiteral') {
quasi.value.raw += message.quasis[0].value.raw + q.value.raw;
out.quasis.push(...message.quasis.slice(1));
out.expressions.push(...message.expressions);
quasi = message.quasis[message.quasis.length - 1];
const m = structuredClone(message);
quasi.value.raw += m.quasis[0].value.raw;
out.quasis.push(...m.quasis.slice(1));
out.expressions.push(...m.expressions);
quasi = m.quasis[m.quasis.length - 1];
quasi.value.raw += q.value.raw;
continue;
}
}

@ -7,11 +7,10 @@ import { DEV } from 'esm-env';
*/
export function CODE(PARAMETER) {
if (DEV) {
const error = new Error(`${'CODE'}\n${MESSAGE}`);
const error = new Error(`${'CODE'}\n${MESSAGE}\nhttps://svelte.dev/e/${'CODE'}`);
error.name = 'Svelte error';
throw error;
} else {
// TODO print a link to the documentation
throw new Error('CODE');
throw new Error(`https://svelte.dev/e/${'CODE'}`);
}
}

@ -9,9 +9,12 @@ var normal = 'font-weight: normal';
*/
export function CODE(PARAMETER) {
if (DEV) {
console.warn(`%c[svelte] ${'CODE'}\n%c${MESSAGE}`, bold, normal);
console.warn(
`%c[svelte] ${'CODE'}\n%c${MESSAGE}\nhttps://svelte.dev/e/${'CODE'}`,
bold,
normal
);
} else {
// TODO print a link to the documentation
console.warn('CODE');
console.warn(`https://svelte.dev/e/${'CODE'}`);
}
}

@ -56,5 +56,5 @@ function e(node, code, message) {
* @returns {never}
*/
export function CODE(node, PARAMETER) {
e(node, 'CODE', MESSAGE);
e(node, 'CODE', `${MESSAGE}\nhttps://svelte.dev/e/${'CODE'}`);
}

@ -47,5 +47,5 @@ export const codes = CODES;
* @param {string} PARAMETER
*/
export function CODE(node, PARAMETER) {
w(node, 'CODE', MESSAGE);
w(node, 'CODE', `${MESSAGE}\nhttps://svelte.dev/e/${'CODE'}`);
}

@ -4,7 +4,7 @@
* @returns {never}
*/
export function CODE(PARAMETER) {
const error = new Error(`${'CODE'}\n${MESSAGE}`);
const error = new Error(`${'CODE'}\n${MESSAGE}\nhttps://svelte.dev/e/${'CODE'}`);
error.name = 'Svelte error';
throw error;
}

@ -7,11 +7,10 @@ import { DEV } from 'esm-env';
*/
export function CODE(PARAMETER) {
if (DEV) {
const error = new Error(`${'CODE'}\n${MESSAGE}`);
const error = new Error(`${'CODE'}\n${MESSAGE}\nhttps://svelte.dev/e/${'CODE'}`);
error.name = 'Svelte error';
throw error;
} else {
// TODO print a link to the documentation
throw new Error('CODE');
throw new Error(`https://svelte.dev/e/${'CODE'}`);
}
}

@ -9,9 +9,12 @@ var normal = 'font-weight: normal';
*/
export function CODE(PARAMETER) {
if (DEV) {
console.warn(`%c[svelte] ${'CODE'}\n%c${MESSAGE}`, bold, normal);
console.warn(
`%c[svelte] ${'CODE'}\n%c${MESSAGE}\nhttps://svelte.dev/e/${'CODE'}`,
bold,
normal
);
} else {
// TODO print a link to the documentation
console.warn('CODE');
console.warn(`https://svelte.dev/e/${'CODE'}`);
}
}

File diff suppressed because it is too large Load Diff

@ -76,7 +76,7 @@ export function compileModule(source, options) {
*
* @overload
* @param {string} source
* @param {{ filename?: string; modern: true }} options
* @param {{ filename?: string; modern: true; loose?: boolean }} options
* @returns {AST.Root}
*/
@ -88,7 +88,7 @@ export function compileModule(source, options) {
*
* @overload
* @param {string} source
* @param {{ filename?: string; modern?: false }} [options]
* @param {{ filename?: string; modern?: false; loose?: boolean }} [options]
* @returns {Record<string, any>}
*/
@ -98,16 +98,18 @@ export function compileModule(source, options) {
* The `modern` option (`false` by default in Svelte 5) makes the parser return a modern AST instead of the legacy AST.
* `modern` will become `true` by default in Svelte 6, and the option will be removed in Svelte 7.
*
* The `loose` option, available since 5.13.0, tries to always return an AST even if the input will not successfully compile.
*
* @param {string} source
* @param {{ filename?: string; rootDir?: string; modern?: boolean }} [options]
* @param {{ filename?: string; rootDir?: string; modern?: boolean; loose?: boolean }} [options]
* @returns {AST.Root | LegacyRoot}
*/
export function parse(source, { filename, rootDir, modern } = {}) {
export function parse(source, { filename, rootDir, modern, loose } = {}) {
source = remove_bom(source);
state.reset_warning_filter(() => false);
state.reset(source, { filename: filename ?? '(unknown)', rootDir });
const ast = _parse(source);
const ast = _parse(source, loose);
return to_public_ast(source, ast, modern);
}

@ -1,5 +1,5 @@
/** @import { Expression } from 'estree' */
/** @import { AST, SvelteNode, TemplateNode } from '#compiler' */
/** @import { AST } from '#compiler' */
/** @import * as Legacy from './types/legacy-nodes.js' */
import { walk } from 'zimmerframe';
import {
@ -11,7 +11,7 @@ import { extract_svelte_ignore } from './utils/extract_svelte_ignore.js';
/**
* Some of the legacy Svelte AST nodes remove whitespace from the start and end of their children.
* @param {TemplateNode[]} nodes
* @param {AST.TemplateNode[]} nodes
*/
function remove_surrounding_whitespace_nodes(nodes) {
const first = nodes.at(0);
@ -40,7 +40,7 @@ function remove_surrounding_whitespace_nodes(nodes) {
* @returns {Legacy.LegacyRoot}
*/
export function convert(source, ast) {
const root = /** @type {SvelteNode | Legacy.LegacySvelteNode} */ (ast);
const root = /** @type {AST.SvelteNode | Legacy.LegacySvelteNode} */ (ast);
return /** @type {Legacy.LegacyRoot} */ (
walk(root, null, {

@ -2,7 +2,7 @@
/** @import { Visitors } from 'zimmerframe' */
/** @import { ComponentAnalysis } from '../phases/types.js' */
/** @import { Scope, ScopeRoot } from '../phases/scope.js' */
/** @import { AST, Binding, SvelteNode, ValidatedCompileOptions } from '#compiler' */
/** @import { AST, Binding, ValidatedCompileOptions } from '#compiler' */
import MagicString from 'magic-string';
import { walk } from 'zimmerframe';
import { parse } from '../phases/1-parse/index.js';
@ -479,7 +479,7 @@ export function migrate(source, { filename, use_ts } = {}) {
* }} State
*/
/** @type {Visitors<SvelteNode, State>} */
/** @type {Visitors<AST.SvelteNode, State>} */
const instance_script = {
_(node, { state, next }) {
// @ts-expect-error
@ -1050,7 +1050,7 @@ function trim_block(state, start, end) {
}
}
/** @type {Visitors<SvelteNode, State>} */
/** @type {Visitors<AST.SvelteNode, State>} */
const template = {
Identifier(node, { state, path }) {
handle_identifier(node, state, path);
@ -1410,7 +1410,7 @@ const template = {
/**
* @param {AST.RegularElement | AST.SvelteElement | AST.SvelteComponent | AST.Component | AST.SlotElement | AST.SvelteFragment} node
* @param {SvelteNode[]} path
* @param {AST.SvelteNode[]} path
* @param {State} state
*/
function migrate_slot_usage(node, path, state) {
@ -1580,7 +1580,7 @@ function migrate_slot_usage(node, path, state) {
/**
* @param {VariableDeclarator} declarator
* @param {State} state
* @param {SvelteNode[]} path
* @param {AST.SvelteNode[]} path
*/
function extract_type_and_comment(declarator, state, path) {
const str = state.str;

@ -1,4 +1,4 @@
/** @import { AST, TemplateNode } from '#compiler' */
/** @import { AST } from '#compiler' */
// @ts-expect-error acorn type definitions are borked in the release we use
import { isIdentifierStart, isIdentifierChar } from 'acorn';
import fragment from './state/fragment.js';
@ -22,13 +22,26 @@ export class Parser {
*/
template;
/**
* @readonly
* @type {string}
*/
template_untrimmed;
/**
* Whether or not we're in loose parsing mode, in which
* case we try to continue parsing as much as possible
* @type {boolean}
*/
loose;
/** */
index = 0;
/** Whether we're parsing in TypeScript mode */
ts = false;
/** @type {TemplateNode[]} */
/** @type {AST.TemplateNode[]} */
stack = [];
/** @type {AST.Fragment[]} */
@ -43,12 +56,17 @@ export class Parser {
/** @type {LastAutoClosedTag | undefined} */
last_auto_closed_tag;
/** @param {string} template */
constructor(template) {
/**
* @param {string} template
* @param {boolean} loose
*/
constructor(template, loose) {
if (typeof template !== 'string') {
throw new TypeError('Template must be a string');
}
this.loose = loose;
this.template_untrimmed = template;
this.template = template.trimEnd();
let match_lang;
@ -88,7 +106,9 @@ export class Parser {
if (this.stack.length > 1) {
const current = this.current();
if (current.type === 'RegularElement') {
if (this.loose) {
current.end = this.template.length;
} else if (current.type === 'RegularElement') {
current.end = current.start + 1;
e.element_unclosed(current, current.name);
} else {
@ -151,14 +171,15 @@ export class Parser {
/**
* @param {string} str
* @param {boolean} required
* @param {boolean} required_in_loose
*/
eat(str, required = false) {
eat(str, required = false, required_in_loose = true) {
if (this.match(str)) {
this.index += str.length;
return true;
}
if (required) {
if (required && (!this.loose || required_in_loose)) {
e.expected_token(this.index, str);
}
@ -233,6 +254,7 @@ export class Parser {
/** @param {RegExp} pattern */
read_until(pattern) {
if (this.index >= this.template.length) {
if (this.loose) return '';
e.unexpected_eof(this.template.length);
}
@ -274,10 +296,11 @@ export class Parser {
/**
* @param {string} template
* @param {boolean} [loose]
* @returns {AST.Root}
*/
export function parse(template) {
const parser = new Parser(template);
export function parse(template, loose = false) {
const parser = new Parser(template, loose);
return parser.root;
}

@ -3,6 +3,7 @@
import { parse_expression_at } from '../acorn.js';
import { regex_whitespace } from '../../patterns.js';
import * as e from '../../../errors.js';
import { find_matching_bracket } from '../utils/bracket.js';
/**
* @param {Parser} parser
@ -39,6 +40,22 @@ export default function read_expression(parser) {
return /** @type {Expression} */ (node);
} catch (err) {
if (parser.loose) {
// Find the next } and treat it as the end of the expression
const end = find_matching_bracket(parser.template, parser.index, '{');
if (end) {
const start = parser.index;
parser.index = end;
// We don't know what the expression is and signal this by returning an empty identifier
return {
type: 'Identifier',
start,
end,
name: ''
};
}
}
parser.acorn_error(err);
}
}

@ -1,5 +1,5 @@
/** @import { Program } from 'estree' */
/** @import { AST, Directive } from '#compiler' */
/** @import { AST } from '#compiler' */
/** @import { Parser } from '../index.js' */
import * as acorn from '../acorn.js';
import { regex_not_newline_characters } from '../../patterns.js';
@ -16,7 +16,7 @@ const ALLOWED_ATTRIBUTES = ['context', 'generics', 'lang', 'module'];
/**
* @param {Parser} parser
* @param {number} start
* @param {Array<AST.Attribute | AST.SpreadAttribute | Directive>} attributes
* @param {Array<AST.Attribute | AST.SpreadAttribute | AST.Directive>} attributes
* @returns {AST.Script}
*/
export function read_script(parser, start, attributes) {

@ -1,4 +1,4 @@
/** @import { AST, Css, Directive } from '#compiler' */
/** @import { AST } from '#compiler' */
/** @import { Parser } from '../index.js' */
import * as e from '../../../errors.js';
@ -18,8 +18,8 @@ const REGEX_HTML_COMMENT_CLOSE = /-->/;
/**
* @param {Parser} parser
* @param {number} start
* @param {Array<AST.Attribute | AST.SpreadAttribute | Directive>} attributes
* @returns {Css.StyleSheet}
* @param {Array<AST.Attribute | AST.SpreadAttribute | AST.Directive>} attributes
* @returns {AST.CSS.StyleSheet}
*/
export default function read_style(parser, start, attributes) {
const content_start = parser.index;
@ -49,7 +49,7 @@ export default function read_style(parser, start, attributes) {
* @returns {any[]}
*/
function read_body(parser, close) {
/** @type {Array<Css.Rule | Css.Atrule>} */
/** @type {Array<AST.CSS.Rule | AST.CSS.Atrule>} */
const children = [];
while (parser.index < parser.template.length) {
@ -71,7 +71,7 @@ function read_body(parser, close) {
/**
* @param {Parser} parser
* @returns {Css.Atrule}
* @returns {AST.CSS.Atrule}
*/
function read_at_rule(parser) {
const start = parser.index;
@ -81,7 +81,7 @@ function read_at_rule(parser) {
const prelude = read_value(parser);
/** @type {Css.Block | null} */
/** @type {AST.CSS.Block | null} */
let block = null;
if (parser.match('{')) {
@ -104,7 +104,7 @@ function read_at_rule(parser) {
/**
* @param {Parser} parser
* @returns {Css.Rule}
* @returns {AST.CSS.Rule}
*/
function read_rule(parser) {
const start = parser.index;
@ -126,10 +126,10 @@ function read_rule(parser) {
/**
* @param {Parser} parser
* @param {boolean} [inside_pseudo_class]
* @returns {Css.SelectorList}
* @returns {AST.CSS.SelectorList}
*/
function read_selector_list(parser, inside_pseudo_class = false) {
/** @type {Css.ComplexSelector[]} */
/** @type {AST.CSS.ComplexSelector[]} */
const children = [];
allow_comment_or_whitespace(parser);
@ -162,18 +162,18 @@ function read_selector_list(parser, inside_pseudo_class = false) {
/**
* @param {Parser} parser
* @param {boolean} [inside_pseudo_class]
* @returns {Css.ComplexSelector}
* @returns {AST.CSS.ComplexSelector}
*/
function read_selector(parser, inside_pseudo_class = false) {
const list_start = parser.index;
/** @type {Css.RelativeSelector[]} */
/** @type {AST.CSS.RelativeSelector[]} */
const children = [];
/**
* @param {Css.Combinator | null} combinator
* @param {AST.CSS.Combinator | null} combinator
* @param {number} start
* @returns {Css.RelativeSelector}
* @returns {AST.CSS.RelativeSelector}
*/
function create_selector(combinator, start) {
return {
@ -190,7 +190,7 @@ function read_selector(parser, inside_pseudo_class = false) {
};
}
/** @type {Css.RelativeSelector} */
/** @type {AST.CSS.RelativeSelector} */
let relative_selector = create_selector(null, parser.index);
while (parser.index < parser.template.length) {
@ -247,7 +247,7 @@ function read_selector(parser, inside_pseudo_class = false) {
} else if (parser.eat(':')) {
const name = read_identifier(parser);
/** @type {null | Css.SelectorList} */
/** @type {null | AST.CSS.SelectorList} */
let args = null;
if (parser.eat('(')) {
@ -372,7 +372,7 @@ function read_selector(parser, inside_pseudo_class = false) {
/**
* @param {Parser} parser
* @returns {Css.Combinator | null}
* @returns {AST.CSS.Combinator | null}
*/
function read_combinator(parser) {
const start = parser.index;
@ -407,14 +407,14 @@ function read_combinator(parser) {
/**
* @param {Parser} parser
* @returns {Css.Block}
* @returns {AST.CSS.Block}
*/
function read_block(parser) {
const start = parser.index;
parser.eat('{', true);
/** @type {Array<Css.Declaration | Css.Rule | Css.Atrule>} */
/** @type {Array<AST.CSS.Declaration | AST.CSS.Rule | AST.CSS.Atrule>} */
const children = [];
while (parser.index < parser.template.length) {
@ -441,7 +441,7 @@ function read_block(parser) {
* Reads a declaration, rule or at-rule
*
* @param {Parser} parser
* @returns {Css.Declaration | Css.Rule | Css.Atrule}
* @returns {AST.CSS.Declaration | AST.CSS.Rule | AST.CSS.Atrule}
*/
function read_block_item(parser) {
if (parser.match('@')) {
@ -460,7 +460,7 @@ function read_block_item(parser) {
/**
* @param {Parser} parser
* @returns {Css.Declaration}
* @returns {AST.CSS.Declaration}
*/
function read_declaration(parser) {
const start = parser.index;

@ -1,5 +1,5 @@
/** @import { Expression } from 'estree' */
/** @import { AST, Directive, ElementLike, TemplateNode } from '#compiler' */
/** @import { AST } from '#compiler' */
/** @import { Parser } from '../index.js' */
import { is_void } from '../../../../utils.js';
import read_expression from '../read/expression.js';
@ -9,10 +9,11 @@ import { decode_character_references } from '../utils/html.js';
import * as e from '../../../errors.js';
import * as w from '../../../warnings.js';
import { create_fragment } from '../utils/create.js';
import { create_attribute, create_expression_metadata } from '../../nodes.js';
import { create_attribute, create_expression_metadata, is_element_node } from '../../nodes.js';
import { get_attribute_expression, is_expression_attribute } from '../../../utils/ast.js';
import { closing_tag_omitted } from '../../../../html-tree-validation.js';
import { list } from '../../../utils/string.js';
import { regex_whitespace } from '../../patterns.js';
const regex_invalid_unquoted_attribute_value = /^(\/>|[\s"'=<>`])/;
const regex_closing_textarea_tag = /^<\/textarea(\s[^>]*)?>/i;
@ -28,7 +29,7 @@ export const regex_valid_component_name =
// (must start with uppercase letter if no dots, can contain dots)
/^(?:\p{Lu}[$\u200c\u200d\p{ID_Continue}.]*|\p{ID_Start}[$\u200c\u200d\p{ID_Continue}]*(?:\.[$\u200c\u200d\p{ID_Continue}]+)+)$/u;
/** @type {Map<string, ElementLike['type']>} */
/** @type {Map<string, AST.ElementLike['type']>} */
const root_only_meta_tags = new Map([
['svelte:head', 'SvelteHead'],
['svelte:options', 'SvelteOptions'],
@ -37,7 +38,7 @@ const root_only_meta_tags = new Map([
['svelte:body', 'SvelteBody']
]);
/** @type {Map<string, ElementLike['type']>} */
/** @type {Map<string, AST.ElementLike['type']>} */
const meta_tags = new Map([
...root_only_meta_tags,
['svelte:element', 'SvelteElement'],
@ -80,7 +81,19 @@ export default function element(parser) {
// close any elements that don't have their own closing tags, e.g. <div><p></div>
while (/** @type {AST.RegularElement} */ (parent).name !== name) {
if (parent.type !== 'RegularElement') {
if (parser.loose) {
// If the previous element did interpret the next opening tag as an attribute, backtrack
if (is_element_node(parent)) {
const last = parent.attributes.at(-1);
if (last?.type === 'Attribute' && last.name === `<${name}`) {
parser.index = last.start;
parent.attributes.pop();
break;
}
}
}
if (parent.type !== 'RegularElement' && !parser.loose) {
if (parser.last_auto_closed_tag && parser.last_auto_closed_tag.tag === name) {
e.element_invalid_closing_tag_autoclosed(start, name, parser.last_auto_closed_tag.reason);
} else {
@ -137,7 +150,7 @@ export default function element(parser) {
? 'SlotElement'
: 'RegularElement';
/** @type {ElementLike} */
/** @type {AST.ElementLike} */
const element =
type === 'RegularElement'
? {
@ -155,7 +168,7 @@ export default function element(parser) {
path: []
}
}
: /** @type {ElementLike} */ ({
: /** @type {AST.ElementLike} */ ({
type,
start,
end: -1,
@ -319,10 +332,41 @@ export default function element(parser) {
parser.append(element);
const self_closing = parser.eat('/') || is_void(name);
const closed = parser.eat('>', true, false);
// Loose parsing mode
if (!closed) {
// We may have eaten an opening `<` of the next element and treated it as an attribute...
const last = element.attributes.at(-1);
if (last?.type === 'Attribute' && last.name === '<') {
parser.index = last.start;
element.attributes.pop();
} else {
// ... or we may have eaten part of a following block ...
const prev_1 = parser.template[parser.index - 1];
const prev_2 = parser.template[parser.index - 2];
const current = parser.template[parser.index];
if (prev_2 === '{' && prev_1 === '/') {
parser.index -= 2;
} else if (prev_1 === '{' && (current === '#' || current === '@' || current === ':')) {
parser.index -= 1;
} else {
// ... or we're followed by whitespace, for example near the end of the template,
// which we want to take in so that language tools has more room to work with
parser.allow_whitespace();
if (parser.index === parser.template.length) {
while (
parser.index < parser.template_untrimmed.length &&
regex_whitespace.test(parser.template_untrimmed[parser.index])
) {
parser.index++;
}
}
}
}
}
parser.eat('>', true);
if (self_closing) {
if (self_closing || !closed) {
// don't push self-closing elements onto the stack
element.end = parser.index;
} else if (name === 'textarea') {
@ -358,7 +402,7 @@ export default function element(parser) {
}
}
/** @param {TemplateNode[]} stack */
/** @param {AST.TemplateNode[]} stack */
function parent_is_head(stack) {
let i = stack.length;
while (i--) {
@ -369,7 +413,7 @@ function parent_is_head(stack) {
return false;
}
/** @param {TemplateNode[]} stack */
/** @param {AST.TemplateNode[]} stack */
function parent_is_shadowroot_template(stack) {
// https://developer.chrome.com/docs/css-ui/declarative-shadow-dom#building_a_declarative_shadow_root
let i = stack.length;
@ -433,7 +477,7 @@ function read_static_attribute(parser) {
/**
* @param {Parser} parser
* @returns {AST.Attribute | AST.SpreadAttribute | Directive | null}
* @returns {AST.Attribute | AST.SpreadAttribute | AST.Directive | null}
*/
function read_attribute(parser) {
const start = parser.index;
@ -461,10 +505,22 @@ function read_attribute(parser) {
return spread;
} else {
const value_start = parser.index;
const name = parser.read_identifier();
let name = parser.read_identifier();
if (name === null) {
e.attribute_empty_shorthand(start);
if (
parser.loose &&
(parser.match('#') || parser.match('/') || parser.match('@') || parser.match(':'))
) {
// We're likely in an unclosed opening tag and did read part of a block.
// Return null to not crash the parser so it can continue with closing the tag.
return null;
} else if (parser.loose && parser.match('}')) {
// Likely in the middle of typing, just created the shorthand
name = '';
} else {
e.attribute_empty_shorthand(start);
}
}
parser.allow_whitespace();
@ -504,8 +560,24 @@ function read_attribute(parser) {
let value = true;
if (parser.eat('=')) {
parser.allow_whitespace();
value = read_attribute_value(parser);
end = parser.index;
if (parser.template[parser.index] === '/' && parser.template[parser.index + 1] === '>') {
const char_start = parser.index;
parser.index++; // consume '/'
value = [
{
start: char_start,
end: char_start + 1,
type: 'Text',
raw: '/',
data: '/'
}
];
end = parser.index;
} else {
value = read_attribute_value(parser);
end = parser.index;
}
} else if (parser.match_regex(regex_starts_with_quote_characters)) {
e.expected_token(parser.index, '=');
}
@ -548,7 +620,7 @@ function read_attribute(parser) {
}
}
/** @type {Directive} */
/** @type {AST.Directive} */
const directive = {
start,
end,
@ -740,5 +812,9 @@ function read_sequence(parser, done, location) {
}
}
e.unexpected_eof(parser.template.length);
if (parser.loose) {
return chunks;
} else {
e.unexpected_eof(parser.template.length);
}
}

@ -279,34 +279,43 @@ function open(parser) {
parser.require_whitespace();
const name_start = parser.index;
const name = parser.read_identifier();
let name = parser.read_identifier();
const name_end = parser.index;
if (name === null) {
e.expected_identifier(parser.index);
if (parser.loose) {
name = '';
} else {
e.expected_identifier(parser.index);
}
}
parser.allow_whitespace();
const params_start = parser.index;
parser.eat('(', true);
let parentheses = 1;
const matched = parser.eat('(', true, false);
while (parser.index < parser.template.length && (!parser.match(')') || parentheses !== 1)) {
if (parser.match('(')) parentheses++;
if (parser.match(')')) parentheses--;
parser.index += 1;
}
if (matched) {
let parentheses = 1;
while (parser.index < parser.template.length && (!parser.match(')') || parentheses !== 1)) {
if (parser.match('(')) parentheses++;
if (parser.match(')')) parentheses--;
parser.index += 1;
}
parser.eat(')', true);
parser.eat(')', true);
}
const prelude = parser.template.slice(0, params_start).replace(/\S/g, ' ');
const params = parser.template.slice(params_start, parser.index);
let function_expression = /** @type {ArrowFunctionExpression} */ (
parse_expression_at(prelude + `${params} => {}`, parser.ts, params_start)
);
let function_expression = matched
? /** @type {ArrowFunctionExpression} */ (
parse_expression_at(prelude + `${params} => {}`, parser.ts, params_start)
)
: { params: [] };
parser.allow_whitespace();
parser.eat('}', true);
@ -454,43 +463,66 @@ function close(parser) {
const start = parser.index - 1;
let block = parser.current();
/** Only relevant/reached for loose parsing mode */
let matched;
switch (block.type) {
case 'IfBlock':
parser.eat('if', true);
matched = parser.eat('if', true, false);
if (!matched) {
block.end = start - 1;
parser.pop();
close(parser);
return;
}
parser.allow_whitespace();
parser.eat('}', true);
while (block.elseif) {
block.end = parser.index;
parser.stack.pop();
block = /** @type {AST.IfBlock} */ (parser.current());
}
block.end = parser.index;
parser.pop();
return;
case 'EachBlock':
parser.eat('each', true);
matched = parser.eat('each', true, false);
break;
case 'KeyBlock':
parser.eat('key', true);
matched = parser.eat('key', true, false);
break;
case 'AwaitBlock':
parser.eat('await', true);
matched = parser.eat('await', true, false);
break;
case 'SnippetBlock':
parser.eat('snippet', true);
matched = parser.eat('snippet', true, false);
break;
case 'RegularElement':
// TODO handle implicitly closed elements
e.block_unexpected_close(start);
if (parser.loose) {
matched = false;
} else {
// TODO handle implicitly closed elements
e.block_unexpected_close(start);
}
break;
default:
e.block_unexpected_close(start);
}
if (!matched) {
block.end = start - 1;
parser.pop();
close(parser);
return;
}
parser.allow_whitespace();
parser.eat('}', true);
block.end = parser.index;

@ -1,3 +1,5 @@
import full_char_code_at from './full_char_code_at.js';
const SQUARE_BRACKET_OPEN = '['.charCodeAt(0);
const SQUARE_BRACKET_CLOSE = ']'.charCodeAt(0);
const CURLY_BRACKET_OPEN = '{'.charCodeAt(0);
@ -33,3 +35,137 @@ export function get_bracket_close(open) {
return CURLY_BRACKET_CLOSE;
}
}
/**
* @param {number} num
* @returns {number} Infinity if {@link num} is negative, else {@link num}.
*/
function infinity_if_negative(num) {
if (num < 0) {
return Infinity;
}
return num;
}
/**
* @param {string} string The string to search.
* @param {number} search_start_index The index to start searching at.
* @param {"'" | '"' | '`'} string_start_char The character that started this string.
* @returns {number} The index of the end of this string expression, or `Infinity` if not found.
*/
function find_string_end(string, search_start_index, string_start_char) {
let string_to_search;
if (string_start_char === '`') {
string_to_search = string;
} else {
// we could slice at the search start index, but this way the index remains valid
string_to_search = string.slice(
0,
infinity_if_negative(string.indexOf('\n', search_start_index))
);
}
return find_unescaped_char(string_to_search, search_start_index, string_start_char);
}
/**
* @param {string} string The string to search.
* @param {number} search_start_index The index to start searching at.
* @returns {number} The index of the end of this regex expression, or `Infinity` if not found.
*/
function find_regex_end(string, search_start_index) {
return find_unescaped_char(string, search_start_index, '/');
}
/**
*
* @param {string} string The string to search.
* @param {number} search_start_index The index to begin the search at.
* @param {string} char The character to search for.
* @returns {number} The index of the first unescaped instance of {@link char}, or `Infinity` if not found.
*/
function find_unescaped_char(string, search_start_index, char) {
let i = search_start_index;
while (true) {
const found_index = string.indexOf(char, i);
if (found_index === -1) {
return Infinity;
}
if (count_leading_backslashes(string, found_index - 1) % 2 === 0) {
return found_index;
}
i = found_index + 1;
}
}
/**
* Count consecutive leading backslashes before {@link search_start_index}.
*
* @example
* ```js
* count_leading_backslashes('\\\\\\foo', 2); // 3 (the backslashes have to be escaped in the string literal, there are three in reality)
* ```
*
* @param {string} string The string to search.
* @param {number} search_start_index The index to begin the search at.
*/
function count_leading_backslashes(string, search_start_index) {
let i = search_start_index;
let count = 0;
while (string[i] === '\\') {
count++;
i--;
}
return count;
}
/**
* Finds the corresponding closing bracket, ignoring brackets found inside comments, strings, or regex expressions.
* @param {string} template The string to search.
* @param {number} index The index to begin the search at.
* @param {string} open The opening bracket (ex: `'{'` will search for `'}'`).
* @returns {number | undefined} The index of the closing bracket, or undefined if not found.
*/
export function find_matching_bracket(template, index, open) {
const open_code = full_char_code_at(open, 0);
const close_code = get_bracket_close(open_code);
let brackets = 1;
let i = index;
while (brackets > 0 && i < template.length) {
const char = template[i];
switch (char) {
case "'":
case '"':
case '`':
i = find_string_end(template, i + 1, char) + 1;
continue;
case '/': {
const next_char = template[i + 1];
if (!next_char) continue;
if (next_char === '/') {
i = infinity_if_negative(template.indexOf('\n', i + 1)) + '\n'.length;
continue;
}
if (next_char === '*') {
i = infinity_if_negative(template.indexOf('*/', i + 1)) + '*/'.length;
continue;
}
i = find_regex_end(template, i + 1) + '/'.length;
continue;
}
default: {
const code = full_char_code_at(template, i);
if (code === open_code) {
brackets++;
} else if (code === close_code) {
brackets--;
}
if (brackets === 0) {
return i;
}
i++;
}
}
}
return undefined;
}

@ -1,5 +1,5 @@
/** @import { ComponentAnalysis } from '../../types.js' */
/** @import { Css } from '#compiler' */
/** @import { AST } from '#compiler' */
/** @import { Visitors } from 'zimmerframe' */
import { walk } from 'zimmerframe';
import * as e from '../../../errors.js';
@ -8,17 +8,17 @@ import { is_global, is_unscoped_pseudo_class } from './utils.js';
/**
* @typedef {Visitors<
* Css.Node,
* AST.CSS.Node,
* {
* keyframes: string[];
* rule: Css.Rule | null;
* rule: AST.CSS.Rule | null;
* }
* >} CssVisitors
*/
/**
* True if is `:global`
* @param {Css.SimpleSelector} simple_selector
* @param {AST.CSS.SimpleSelector} simple_selector
*/
function is_global_block_selector(simple_selector) {
return (
@ -112,7 +112,7 @@ const css_visitors = {
}
},
RelativeSelector(node, context) {
const parent = /** @type {Css.ComplexSelector} */ (context.path.at(-1));
const parent = /** @type {AST.CSS.ComplexSelector} */ (context.path.at(-1));
if (
node.combinator != null &&
@ -149,7 +149,7 @@ const css_visitors = {
if (node.metadata.is_global_like || node.metadata.is_global) {
// So that nested selectors like `:root:not(.x)` are not marked as unused
for (const child of node.selectors) {
walk(/** @type {Css.Node} */ (child), null, {
walk(/** @type {AST.CSS.Node} */ (child), null, {
ComplexSelector(node, context) {
node.metadata.used = true;
context.next();
@ -177,7 +177,7 @@ const css_visitors = {
if (idx !== -1) {
is_global_block = true;
for (let i = idx + 1; i < child.selectors.length; i++) {
walk(/** @type {Css.Node} */ (child.selectors[i]), null, {
walk(/** @type {AST.CSS.Node} */ (child.selectors[i]), null, {
ComplexSelector(node) {
node.metadata.used = true;
}
@ -240,7 +240,7 @@ const css_visitors = {
});
},
NestingSelector(node, context) {
const rule = /** @type {Css.Rule} */ (context.state.rule);
const rule = /** @type {AST.CSS.Rule} */ (context.state.rule);
const parent_rule = rule.metadata.parent_rule;
if (!parent_rule) {
@ -271,7 +271,7 @@ const css_visitors = {
};
/**
* @param {Css.StyleSheet} stylesheet
* @param {AST.CSS.StyleSheet} stylesheet
* @param {ComponentAnalysis} analysis
*/
export function analyze_css(stylesheet, analysis) {

@ -14,7 +14,7 @@ const whitelist_attribute_selector = new Map([
['dialog', ['open']]
]);
/** @type {Compiler.Css.Combinator} */
/** @type {Compiler.AST.CSS.Combinator} */
const descendant_combinator = {
type: 'Combinator',
name: ' ',
@ -22,7 +22,7 @@ const descendant_combinator = {
end: -1
};
/** @type {Compiler.Css.RelativeSelector} */
/** @type {Compiler.AST.CSS.RelativeSelector} */
const nesting_selector = {
type: 'RelativeSelector',
start: -1,
@ -51,11 +51,11 @@ const seen = new Set();
/**
*
* @param {Compiler.Css.StyleSheet} stylesheet
* @param {Compiler.AST.CSS.StyleSheet} stylesheet
* @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement} element
*/
export function prune(stylesheet, element) {
walk(/** @type {Compiler.Css.Node} */ (stylesheet), null, {
walk(/** @type {Compiler.AST.CSS.Node} */ (stylesheet), null, {
Rule(node, context) {
if (node.metadata.is_global_block) {
context.visit(node.prelude);
@ -69,7 +69,11 @@ export function prune(stylesheet, element) {
seen.clear();
if (
apply_selector(selectors, /** @type {Compiler.Css.Rule} */ (node.metadata.rule), element)
apply_selector(
selectors,
/** @type {Compiler.AST.CSS.Rule} */ (node.metadata.rule),
element
)
) {
node.metadata.used = true;
}
@ -86,7 +90,7 @@ export function prune(stylesheet, element) {
* Also searches them for any existing `&` selectors and adds one if none are found.
* This ensures we traverse up to the parent rule when the inner selectors match and we're
* trying to see if the parent rule also matches.
* @param {Compiler.Css.ComplexSelector} node
* @param {Compiler.AST.CSS.ComplexSelector} node
*/
function get_relative_selectors(node) {
const selectors = truncate(node);
@ -124,7 +128,7 @@ function get_relative_selectors(node) {
/**
* Discard trailing `:global(...)` selectors, these are unused for scoping purposes
* @param {Compiler.Css.ComplexSelector} node
* @param {Compiler.AST.CSS.ComplexSelector} node
*/
function truncate(node) {
const i = node.children.findLastIndex(({ metadata, selectors }) => {
@ -152,8 +156,8 @@ function truncate(node) {
}
/**
* @param {Compiler.Css.RelativeSelector[]} relative_selectors
* @param {Compiler.Css.Rule} rule
* @param {Compiler.AST.CSS.RelativeSelector[]} relative_selectors
* @param {Compiler.AST.CSS.Rule} rule
* @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement} element
* @returns {boolean}
*/
@ -178,9 +182,9 @@ function apply_selector(relative_selectors, rule, element) {
}
/**
* @param {Compiler.Css.RelativeSelector} relative_selector
* @param {Compiler.Css.RelativeSelector[]} parent_selectors
* @param {Compiler.Css.Rule} rule
* @param {Compiler.AST.CSS.RelativeSelector} relative_selector
* @param {Compiler.AST.CSS.RelativeSelector[]} parent_selectors
* @param {Compiler.AST.CSS.Rule} rule
* @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement | Compiler.AST.RenderTag | Compiler.AST.Component | Compiler.AST.SvelteComponent | Compiler.AST.SvelteSelf} node
* @returns {boolean}
*/
@ -263,8 +267,8 @@ function apply_combinator(relative_selector, parent_selectors, rule, node) {
* it's a `:global(...)` or unscopeable selector, or
* is an `:is(...)` or `:where(...)` selector that contains
* a global selector
* @param {Compiler.Css.RelativeSelector} selector
* @param {Compiler.Css.Rule} rule
* @param {Compiler.AST.CSS.RelativeSelector} selector
* @param {Compiler.AST.CSS.Rule} rule
*/
function is_global(selector, rule) {
if (selector.metadata.is_global || selector.metadata.is_global_like) {
@ -272,7 +276,7 @@ function is_global(selector, rule) {
}
for (const s of selector.selectors) {
/** @type {Compiler.Css.SelectorList | null} */
/** @type {Compiler.AST.CSS.SelectorList | null} */
let selector_list = null;
let owner = rule;
@ -283,7 +287,7 @@ function is_global(selector, rule) {
}
if (s.type === 'NestingSelector') {
owner = /** @type {Compiler.Css.Rule} */ (rule.metadata.parent_rule);
owner = /** @type {Compiler.AST.CSS.Rule} */ (rule.metadata.parent_rule);
selector_list = owner.prelude;
}
@ -306,8 +310,8 @@ const regex_backslash_and_following_character = /\\(.)/g;
/**
* Ensure that `element` satisfies each simple selector in `relative_selector`
*
* @param {Compiler.Css.RelativeSelector} relative_selector
* @param {Compiler.Css.Rule} rule
* @param {Compiler.AST.CSS.RelativeSelector} relative_selector
* @param {Compiler.AST.CSS.Rule} rule
* @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement} element
* @returns {boolean}
*/
@ -352,7 +356,7 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element)
const seen = new Set();
/**
* @param {Compiler.SvelteNode} node
* @param {Compiler.AST.SvelteNode} node
* @param {{ is_child: boolean }} state
*/
function walk_children(node, state) {
@ -389,7 +393,7 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element)
// upwards and back-to-front, we need to first check the selectors inside :has(...), then check the rest of the
// selector in a way that is similar to ancestor matching. In a sense, we're treating `.x:has(.y)` as `.x .y`.
for (const has_selector of has_selectors) {
const complex_selectors = /** @type {Compiler.Css.SelectorList} */ (has_selector.args)
const complex_selectors = /** @type {Compiler.AST.CSS.SelectorList} */ (has_selector.args)
.children;
let matched = false;
@ -578,7 +582,7 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element)
case 'NestingSelector': {
let matched = false;
const parent = /** @type {Compiler.Css.Rule} */ (rule.metadata.parent_rule);
const parent = /** @type {Compiler.AST.CSS.Rule} */ (rule.metadata.parent_rule);
for (const complex_selector of parent.prelude.children) {
if (
@ -611,9 +615,9 @@ function get_following_sibling_elements(element, include_self) {
const path = element.metadata.path;
let i = path.length;
/** @type {Compiler.SvelteNode} */
/** @type {Compiler.AST.SvelteNode} */
let start = element;
let nodes = /** @type {Compiler.SvelteNode[]} */ (
let nodes = /** @type {Compiler.AST.SvelteNode[]} */ (
/** @type {Compiler.AST.Fragment} */ (path[0]).nodes
);
@ -639,7 +643,7 @@ function get_following_sibling_elements(element, include_self) {
const seen = new Set();
/** @param {Compiler.SvelteNode} node */
/** @param {Compiler.AST.SvelteNode} node */
function get_siblings(node) {
walk(node, null, {
RegularElement(node) {
@ -836,7 +840,7 @@ function get_possible_element_siblings(node, adjacent_only, seen = new Set()) {
const result = new Map();
const path = node.metadata.path;
/** @type {Compiler.SvelteNode} */
/** @type {Compiler.AST.SvelteNode} */
let current = node;
let i = path.length;
@ -1008,7 +1012,7 @@ function higher_existence(exist1, exist2) {
}
/**
* @param {Compiler.SvelteNode[]} children
* @param {Compiler.AST.SvelteNode[]} children
* @param {boolean} adjacent_only
*/
function loop_child(children, adjacent_only) {
@ -1038,7 +1042,7 @@ function loop_child(children, adjacent_only) {
}
/**
* @param {Compiler.SvelteNode} node
* @param {Compiler.AST.SvelteNode} node
* @returns {node is Compiler.AST.IfBlock | Compiler.AST.EachBlock | Compiler.AST.AwaitBlock | Compiler.AST.KeyBlock | Compiler.AST.SlotElement}
*/
function is_block(node) {

@ -1,17 +1,17 @@
/** @import { Visitors } from 'zimmerframe' */
/** @import { Css } from '#compiler' */
/** @import { AST } from '#compiler' */
import { walk } from 'zimmerframe';
import * as w from '../../../warnings.js';
import { is_keyframes_node } from '../../css.js';
/**
* @param {Css.StyleSheet} stylesheet
* @param {AST.CSS.StyleSheet} stylesheet
*/
export function warn_unused(stylesheet) {
walk(stylesheet, { stylesheet }, visitors);
}
/** @type {Visitors<Css.Node, { stylesheet: Css.StyleSheet }>} */
/** @type {Visitors<AST.CSS.Node, { stylesheet: AST.CSS.StyleSheet }>} */
const visitors = {
Atrule(node, context) {
if (!is_keyframes_node(node)) {
@ -28,7 +28,7 @@ const visitors = {
!node.metadata.used &&
// prevent double-marking of `.unused:is(.unused)`
(context.path.at(-2)?.type !== 'PseudoClassSelector' ||
/** @type {Css.ComplexSelector} */ (context.path.at(-4))?.metadata.used)
/** @type {AST.CSS.ComplexSelector} */ (context.path.at(-4))?.metadata.used)
) {
const content = context.state.stylesheet.content;
const text = content.styles.substring(node.start - content.start, node.end - content.start);

@ -1,4 +1,4 @@
/** @import { AST, Css } from '#compiler' */
/** @import { AST } from '#compiler' */
/** @import { Node } from 'estree' */
const UNKNOWN = {};
@ -36,7 +36,7 @@ export function get_possible_values(chunk) {
/**
* Returns all parent rules; root is last
* @param {Css.Rule | null} rule
* @param {AST.CSS.Rule | null} rule
*/
export function get_parent_rules(rule) {
const rules = [];
@ -51,8 +51,8 @@ export function get_parent_rules(rule) {
/**
* True if is `:global(...)` or `:global` and no pseudo class that is scoped.
* @param {Css.RelativeSelector} relative_selector
* @returns {relative_selector is Css.RelativeSelector & { selectors: [Css.PseudoClassSelector, ...Array<Css.PseudoClassSelector | Css.PseudoElementSelector>] }}
* @param {AST.CSS.RelativeSelector} relative_selector
* @returns {relative_selector is AST.CSS.RelativeSelector & { selectors: [AST.CSS.PseudoClassSelector, ...Array<AST.CSS.PseudoClassSelector | AST.CSS.PseudoElementSelector>] }}
*/
export function is_global(relative_selector) {
const first = relative_selector.selectors[0];
@ -72,7 +72,7 @@ export function is_global(relative_selector) {
/**
* `true` if is a pseudo class that cannot be or is not scoped
* @param {Css.SimpleSelector} selector
* @param {AST.CSS.SimpleSelector} selector
*/
export function is_unscoped_pseudo_class(selector) {
return (
@ -96,8 +96,8 @@ export function is_unscoped_pseudo_class(selector) {
/**
* True if is `:global(...)` or `:global`, irrespective of whether or not there are any pseudo classes that are scoped.
* Difference to `is_global`: `:global(x):has(y)` is `true` for `is_outer_global` but `false` for `is_global`.
* @param {Css.RelativeSelector} relative_selector
* @returns {relative_selector is Css.RelativeSelector & { selectors: [Css.PseudoClassSelector, ...Array<Css.PseudoClassSelector | Css.PseudoElementSelector>] }}
* @param {AST.CSS.RelativeSelector} relative_selector
* @returns {relative_selector is AST.CSS.RelativeSelector & { selectors: [AST.CSS.PseudoClassSelector, ...Array<AST.CSS.PseudoClassSelector | AST.CSS.PseudoElementSelector>] }}
*/
export function is_outer_global(relative_selector) {
const first = relative_selector.selectors[0];

@ -1,5 +1,5 @@
/** @import { Expression, Node, Program } from 'estree' */
/** @import { Binding, AST, SvelteNode, ValidatedCompileOptions, ValidatedModuleCompileOptions } from '#compiler' */
/** @import { Binding, AST, ValidatedCompileOptions, ValidatedModuleCompileOptions } from '#compiler' */
/** @import { AnalysisState, Visitors } from './types' */
/** @import { Analysis, ComponentAnalysis, Js, ReactiveStatement, Template } from '../types' */
import { walk } from 'zimmerframe';
@ -525,7 +525,7 @@ export function analyze_component(root, source, options) {
// more legacy nonsense: if an `each` binding is reassigned/mutated,
// treat the expression as being mutated as well
walk(/** @type {SvelteNode} */ (template.ast), null, {
walk(/** @type {AST.SvelteNode} */ (template.ast), null, {
EachBlock(node) {
const scope = /** @type {Scope} */ (template.scopes.get(node));
@ -608,7 +608,7 @@ export function analyze_component(root, source, options) {
reactive_statements: new Map()
};
walk(/** @type {SvelteNode} */ (ast), state, visitors);
walk(/** @type {AST.SvelteNode} */ (ast), state, visitors);
}
// warn on any nonstate declarations that are a) reassigned and b) referenced in the template
@ -677,7 +677,7 @@ export function analyze_component(root, source, options) {
function_depth: scope.function_depth
};
walk(/** @type {SvelteNode} */ (ast), state, visitors);
walk(/** @type {AST.SvelteNode} */ (ast), state, visitors);
}
for (const [name, binding] of instance.scope.declarations) {
@ -698,7 +698,7 @@ export function analyze_component(root, source, options) {
}
for (const node of analysis.module.ast.body) {
if (node.type === 'ExportNamedDeclaration' && node.specifiers !== null) {
if (node.type === 'ExportNamedDeclaration' && node.specifiers !== null && node.source == null) {
for (const specifier of node.specifiers) {
if (specifier.local.type !== 'Identifier') continue;

@ -1,11 +1,11 @@
import type { Scope } from '../scope.js';
import type { ComponentAnalysis, ReactiveStatement } from '../types.js';
import type { ExpressionMetadata, AST, ValidatedCompileOptions, SvelteNode } from '#compiler';
import type { AST, ExpressionMetadata, ValidatedCompileOptions } from '#compiler';
import type { LabeledStatement } from 'estree';
export interface AnalysisState {
scope: Scope;
scopes: Map<SvelteNode, Scope>;
scopes: Map<AST.SvelteNode, Scope>;
analysis: ComponentAnalysis;
options: ValidatedCompileOptions;
ast_type: 'instance' | 'template' | 'module';
@ -31,11 +31,11 @@ export interface AnalysisState {
}
export type Context<State extends AnalysisState = AnalysisState> = import('zimmerframe').Context<
SvelteNode,
AST.SvelteNode,
State
>;
export type Visitors<State extends AnalysisState = AnalysisState> = import('zimmerframe').Visitors<
SvelteNode,
AST.SvelteNode,
State
>;

@ -1,5 +1,4 @@
/** @import { AssignmentExpression } from 'estree' */
/** @import { SvelteNode } from '#compiler' */
/** @import { Context } from '../types' */
import { extract_identifiers, object } from '../../../utils/ast.js';
import { validate_assignment } from './shared/utils.js';

@ -1,5 +1,5 @@
/** @import { ArrowFunctionExpression, Expression, FunctionDeclaration, FunctionExpression } from 'estree' */
/** @import { AST, DelegatedEvent, SvelteNode } from '#compiler' */
/** @import { AST, DelegatedEvent } from '#compiler' */
/** @import { Context } from '../types' */
import { cannot_be_set_statically, is_capture_event, is_delegated } from '../../../../utils.js';
import {
@ -16,7 +16,7 @@ import { mark_subtree_dynamic } from './shared/fragment.js';
export function Attribute(node, context) {
context.next();
const parent = /** @type {SvelteNode} */ (context.path.at(-1));
const parent = /** @type {AST.SvelteNode} */ (context.path.at(-1));
if (parent.type === 'RegularElement') {
// special case <option value="" />

@ -11,108 +11,13 @@ import * as w from '../../../warnings.js';
import { binding_properties } from '../../bindings.js';
import fuzzymatch from '../../1-parse/utils/fuzzymatch.js';
import { is_content_editable_binding, is_svg } from '../../../../utils.js';
import { mark_subtree_dynamic } from './shared/fragment.js';
/**
* @param {AST.BindDirective} node
* @param {Context} context
*/
export function BindDirective(node, context) {
validate_no_const_assignment(node, node.expression, context.state.scope, true);
const assignee = node.expression;
const left = object(assignee);
if (left === null) {
e.bind_invalid_expression(node);
}
const binding = context.state.scope.get(left.name);
if (assignee.type === 'Identifier') {
// reassignment
if (
node.name !== 'this' && // bind:this also works for regular variables
(!binding ||
(binding.kind !== 'state' &&
binding.kind !== 'raw_state' &&
binding.kind !== 'prop' &&
binding.kind !== 'bindable_prop' &&
binding.kind !== 'each' &&
binding.kind !== 'store_sub' &&
!binding.updated)) // TODO wut?
) {
e.bind_invalid_value(node.expression);
}
if (context.state.analysis.runes && binding?.kind === 'each') {
e.each_item_invalid_assignment(node);
}
if (binding?.kind === 'snippet') {
e.snippet_parameter_assignment(node);
}
}
if (node.name === 'group') {
if (!binding) {
throw new Error('Cannot find declaration for bind:group');
}
// Traverse the path upwards and find all EachBlocks who are (indirectly) contributing to bind:group,
// i.e. one of their declarations is referenced in the binding. This allows group bindings to work
// correctly when referencing a variable declared in an EachBlock by using the index of the each block
// entries as keys.
const each_blocks = [];
const [keypath, expression_ids] = extract_all_identifiers_from_expression(node.expression);
let ids = expression_ids;
let i = context.path.length;
while (i--) {
const parent = context.path[i];
if (parent.type === 'EachBlock') {
const references = ids.filter((id) => parent.metadata.declarations.has(id.name));
if (references.length > 0) {
parent.metadata.contains_group_binding = true;
each_blocks.push(parent);
ids = ids.filter((id) => !references.includes(id));
ids.push(...extract_all_identifiers_from_expression(parent.expression)[1]);
}
}
}
// The identifiers that make up the binding expression form they key for the binding group.
// If the same identifiers in the same order are used in another bind:group, they will be in the same group.
// (there's an edge case where `bind:group={a[i]}` will be in a different group than `bind:group={a[j]}` even when i == j,
// but this is a limitation of the current static analysis we do; it also never worked in Svelte 4)
const bindings = expression_ids.map((id) => context.state.scope.get(id.name));
let group_name;
outer: for (const [[key, b], group] of context.state.analysis.binding_groups) {
if (b.length !== bindings.length || key !== keypath) continue;
for (let i = 0; i < bindings.length; i++) {
if (bindings[i] !== b[i]) continue outer;
}
group_name = group;
}
if (!group_name) {
group_name = context.state.scope.root.unique('binding_group');
context.state.analysis.binding_groups.set([keypath, bindings], group_name);
}
node.metadata = {
binding_group_name: group_name,
parent_each_blocks: each_blocks
};
}
if (binding?.kind === 'each' && binding.metadata?.inside_rest) {
w.bind_invalid_each_rest(binding.node, binding.node.name);
}
const parent = context.path.at(-1);
if (
@ -218,5 +123,125 @@ export function BindDirective(node, context) {
}
}
// When dealing with bind getters/setters skip the specific binding validation
// Group bindings aren't supported for getter/setters so we don't need to handle
// the metadata
if (node.expression.type === 'SequenceExpression') {
if (node.name === 'group') {
e.bind_group_invalid_expression(node);
}
let i = /** @type {number} */ (node.expression.start);
while (context.state.analysis.source[--i] !== '{') {
if (context.state.analysis.source[i] === '(') {
e.bind_invalid_parens(node, node.name);
}
}
if (node.expression.expressions.length !== 2) {
e.bind_invalid_expression(node);
}
mark_subtree_dynamic(context.path);
return;
}
validate_no_const_assignment(node, node.expression, context.state.scope, true);
const assignee = node.expression;
const left = object(assignee);
if (left === null) {
e.bind_invalid_expression(node);
}
const binding = context.state.scope.get(left.name);
if (assignee.type === 'Identifier') {
// reassignment
if (
node.name !== 'this' && // bind:this also works for regular variables
(!binding ||
(binding.kind !== 'state' &&
binding.kind !== 'raw_state' &&
binding.kind !== 'prop' &&
binding.kind !== 'bindable_prop' &&
binding.kind !== 'each' &&
binding.kind !== 'store_sub' &&
!binding.updated)) // TODO wut?
) {
e.bind_invalid_value(node.expression);
}
if (context.state.analysis.runes && binding?.kind === 'each') {
e.each_item_invalid_assignment(node);
}
if (binding?.kind === 'snippet') {
e.snippet_parameter_assignment(node);
}
}
if (node.name === 'group') {
if (!binding) {
throw new Error('Cannot find declaration for bind:group');
}
// Traverse the path upwards and find all EachBlocks who are (indirectly) contributing to bind:group,
// i.e. one of their declarations is referenced in the binding. This allows group bindings to work
// correctly when referencing a variable declared in an EachBlock by using the index of the each block
// entries as keys.
const each_blocks = [];
const [keypath, expression_ids] = extract_all_identifiers_from_expression(node.expression);
let ids = expression_ids;
let i = context.path.length;
while (i--) {
const parent = context.path[i];
if (parent.type === 'EachBlock') {
const references = ids.filter((id) => parent.metadata.declarations.has(id.name));
if (references.length > 0) {
parent.metadata.contains_group_binding = true;
each_blocks.push(parent);
ids = ids.filter((id) => !references.includes(id));
ids.push(...extract_all_identifiers_from_expression(parent.expression)[1]);
}
}
}
// The identifiers that make up the binding expression form they key for the binding group.
// If the same identifiers in the same order are used in another bind:group, they will be in the same group.
// (there's an edge case where `bind:group={a[i]}` will be in a different group than `bind:group={a[j]}` even when i == j,
// but this is a limitation of the current static analysis we do; it also never worked in Svelte 4)
const bindings = expression_ids.map((id) => context.state.scope.get(id.name));
let group_name;
outer: for (const [[key, b], group] of context.state.analysis.binding_groups) {
if (b.length !== bindings.length || key !== keypath) continue;
for (let i = 0; i < bindings.length; i++) {
if (bindings[i] !== b[i]) continue outer;
}
group_name = group;
}
if (!group_name) {
group_name = context.state.scope.root.unique('binding_group');
context.state.analysis.binding_groups.set([keypath, bindings], group_name);
}
node.metadata = {
binding_group_name: group_name,
parent_each_blocks: each_blocks
};
}
if (binding?.kind === 'each' && binding.metadata?.inside_rest) {
w.bind_invalid_each_rest(binding.node, binding.node.name);
}
context.next();
}

@ -1,5 +1,5 @@
/** @import { CallExpression, VariableDeclarator } from 'estree' */
/** @import { AST, SvelteNode } from '#compiler' */
/** @import { AST } from '#compiler' */
/** @import { Context } from '../types' */
import { get_rune } from '../../scope.js';
import * as e from '../../../errors.js';
@ -12,7 +12,7 @@ import { dev } from '../../../state.js';
* @param {Context} context
*/
export function CallExpression(node, context) {
const parent = /** @type {SvelteNode} */ (get_parent(context.path, -1));
const parent = /** @type {AST.SvelteNode} */ (get_parent(context.path, -1));
const rune = get_rune(node, context.state.scope);

@ -1,5 +1,5 @@
/** @import { Expression, LabeledStatement } from 'estree' */
/** @import { AST, ReactiveStatement, SvelteNode } from '#compiler' */
/** @import { AST, ReactiveStatement } from '#compiler' */
/** @import { Context } from '../types' */
import * as e from '../../../errors.js';
import { extract_identifiers, object } from '../../../utils/ast.js';
@ -11,7 +11,7 @@ import * as w from '../../../warnings.js';
*/
export function LabeledStatement(node, context) {
if (node.label.name === '$') {
const parent = /** @type {SvelteNode} */ (context.path.at(-1));
const parent = /** @type {AST.SvelteNode} */ (context.path.at(-1));
const is_reactive_statement =
context.state.ast_type === 'instance' && parent.type === 'Program';

@ -1,4 +1,4 @@
/** @import { AST, Binding, SvelteNode } from '#compiler' */
/** @import { AST, Binding } from '#compiler' */
/** @import { Scope } from '../../scope' */
/** @import { Context } from '../types' */
import { validate_block_not_empty, validate_opening_tag } from './shared/utils.js';
@ -80,7 +80,7 @@ export function SnippetBlock(node, context) {
}
/**
* @param {Map<SvelteNode, Scope>} scopes
* @param {Map<AST.SvelteNode, Scope>} scopes
* @param {Scope} scope
*/
function can_hoist_snippet(scope, scopes, visited = new Set()) {
@ -99,8 +99,9 @@ function can_hoist_snippet(scope, scopes, visited = new Set()) {
if (binding.initial?.type === 'SnippetBlock') {
if (visited.has(binding)) continue;
visited.add(binding);
const snippet_scope = /** @type {Scope} */ (scopes.get(binding.initial));
if (can_hoist_snippet(binding.scope, scopes, visited)) {
if (can_hoist_snippet(snippet_scope, scopes, visited)) {
continue;
}
}

@ -1,5 +1,5 @@
/** @import { Context } from '../../types.js' */
/** @import { AST, SvelteNode, TemplateNode } from '#compiler' */
/** @import { AST } from '#compiler' */
/** @import { ARIARoleDefinitionKey, ARIARoleRelationConcept, ARIAProperty, ARIAPropertyDefinition, ARIARoleDefinition } from 'aria-query' */
import { roles as roles_map, aria, elementRoles } from 'aria-query';
// @ts-expect-error package doesn't provide typings
@ -582,7 +582,7 @@ function get_implicit_role(name, attribute_map) {
const invisible_elements = ['meta', 'html', 'script', 'style'];
/**
* @param {SvelteNode[]} path
* @param {AST.SvelteNode[]} path
* @param {string[]} elements
*/
function is_parent(path, elements) {
@ -1062,7 +1062,7 @@ export function check_element(node, context) {
}
if (node.name === 'label') {
/** @param {TemplateNode} node */
/** @param {AST.TemplateNode} node */
const has_input_child = (node) => {
let has = false;
walk(

@ -1,4 +1,4 @@
/** @import { AST, ElementLike } from '#compiler' */
/** @import { AST } from '#compiler' */
/** @import { Context } from '../../types' */
import * as e from '../../../../errors.js';
import { is_text_attribute } from '../../../../utils/ast.js';
@ -22,7 +22,7 @@ export function validate_attribute_name(attribute) {
/**
* @param {AST.Attribute} attribute
* @param {ElementLike} parent
* @param {AST.ElementLike} parent
*/
export function validate_attribute(attribute, parent) {
if (

@ -1,7 +1,7 @@
/** @import { AST, SvelteNode } from '#compiler' */
/** @import { AST } from '#compiler' */
/**
* @param {SvelteNode[]} path
* @param {AST.SvelteNode[]} path
*/
export function mark_subtree_dynamic(path) {
let i = path.length;

@ -1,5 +1,5 @@
/** @import * as ESTree from 'estree' */
/** @import { ValidatedCompileOptions, AST, ValidatedModuleCompileOptions, SvelteNode } from '#compiler' */
/** @import { AST, ValidatedCompileOptions, ValidatedModuleCompileOptions } from '#compiler' */
/** @import { ComponentAnalysis, Analysis } from '../../types' */
/** @import { Visitors, ComponentClientTransformState, ClientTransformState } from './types' */
import { walk } from 'zimmerframe';
@ -178,7 +178,7 @@ export function client_component(analysis, options) {
};
const module = /** @type {ESTree.Program} */ (
walk(/** @type {SvelteNode} */ (analysis.module.ast), state, visitors)
walk(/** @type {AST.SvelteNode} */ (analysis.module.ast), state, visitors)
);
const instance_state = {
@ -190,12 +190,12 @@ export function client_component(analysis, options) {
};
const instance = /** @type {ESTree.Program} */ (
walk(/** @type {SvelteNode} */ (analysis.instance.ast), instance_state, visitors)
walk(/** @type {AST.SvelteNode} */ (analysis.instance.ast), instance_state, visitors)
);
const template = /** @type {ESTree.Program} */ (
walk(
/** @type {SvelteNode} */ (analysis.template.ast),
/** @type {AST.SvelteNode} */ (analysis.template.ast),
{
...state,
transform: instance_state.transform,
@ -664,7 +664,7 @@ export function client_module(analysis, options) {
};
const module = /** @type {ESTree.Program} */ (
walk(/** @type {SvelteNode} */ (analysis.module.ast), state, visitors)
walk(/** @type {AST.SvelteNode} */ (analysis.module.ast), state, visitors)
);
return {

@ -9,7 +9,7 @@ import type {
UpdateExpression,
VariableDeclaration
} from 'estree';
import type { Namespace, SvelteNode, ValidatedCompileOptions } from '#compiler';
import type { AST, Namespace, ValidatedCompileOptions } from '#compiler';
import type { TransformState } from '../types.js';
import type { ComponentAnalysis } from '../../types.js';
import type { SourceLocation } from '#shared';
@ -98,14 +98,14 @@ export interface StateField {
id: PrivateIdentifier;
}
export type Context = import('zimmerframe').Context<SvelteNode, ClientTransformState>;
export type Visitors = import('zimmerframe').Visitors<SvelteNode, any>;
export type Context = import('zimmerframe').Context<AST.SvelteNode, ClientTransformState>;
export type Visitors = import('zimmerframe').Visitors<AST.SvelteNode, any>;
export type ComponentContext = import('zimmerframe').Context<
SvelteNode,
AST.SvelteNode,
ComponentClientTransformState
>;
export type ComponentVisitors = import('zimmerframe').Visitors<
SvelteNode,
AST.SvelteNode,
ComponentClientTransformState
>;

@ -1,5 +1,5 @@
/** @import { ArrowFunctionExpression, Expression, FunctionDeclaration, FunctionExpression, Identifier, Pattern, PrivateIdentifier, Statement } from 'estree' */
/** @import { Binding, SvelteNode } from '#compiler' */
/** @import { AST, Binding } from '#compiler' */
/** @import { ClientTransformState, ComponentClientTransformState, ComponentContext } from './types.js' */
/** @import { Analysis } from '../../types.js' */
/** @import { Scope } from '../../scope.js' */
@ -271,7 +271,7 @@ export function should_proxy(node, scope) {
/**
* @param {Pattern} node
* @param {import('zimmerframe').Context<SvelteNode, ComponentClientTransformState>} context
* @param {import('zimmerframe').Context<AST.SvelteNode, ComponentClientTransformState>} context
* @returns {{ id: Pattern, declarations: null | Statement[] }}
*/
export function create_derived_block_argument(node, context) {

@ -169,6 +169,17 @@ function build_assignment(operator, left, right, context) {
}
}
// special case — ignore `bind:prop={getter, (v) => (...)}` / `bind:value={x.y}`
if (
path.at(-1) === 'Component' ||
path.at(-1) === 'SvelteComponent' ||
(path.at(-1) === 'ArrowFunctionExpression' &&
path.at(-2) === 'SequenceExpression' &&
(path.at(-3) === 'Component' || path.at(-3) === 'SvelteComponent'))
) {
should_transform = false;
}
if (left.type === 'MemberExpression' && should_transform) {
const callee = callees[operator];

@ -1,5 +1,5 @@
/** @import { CallExpression, Expression, MemberExpression } from 'estree' */
/** @import { AST, SvelteNode } from '#compiler' */
/** @import { CallExpression, Expression, Pattern } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */
import { dev, is_ignored } from '../../../../state.js';
import { is_text_attribute } from '../../../../utils/ast.js';
@ -13,41 +13,50 @@ import { build_bind_this, validate_binding } from './shared/utils.js';
* @param {ComponentContext} context
*/
export function BindDirective(node, context) {
const expression = node.expression;
const expression = /** @type {Expression} */ (context.visit(node.expression));
const property = binding_properties[node.name];
const parent = /** @type {SvelteNode} */ (context.path.at(-1));
if (
dev &&
context.state.analysis.runes &&
expression.type === 'MemberExpression' &&
(node.name !== 'this' ||
context.path.some(
({ type }) =>
type === 'IfBlock' || type === 'EachBlock' || type === 'AwaitBlock' || type === 'KeyBlock'
)) &&
!is_ignored(node, 'binding_property_non_reactive')
) {
validate_binding(
context.state,
node,
/**@type {MemberExpression} */ (context.visit(expression))
);
}
const parent = /** @type {AST.SvelteNode} */ (context.path.at(-1));
let get, set;
if (expression.type === 'SequenceExpression') {
[get, set] = expression.expressions;
} else {
if (
dev &&
context.state.analysis.runes &&
expression.type === 'MemberExpression' &&
(node.name !== 'this' ||
context.path.some(
({ type }) =>
type === 'IfBlock' ||
type === 'EachBlock' ||
type === 'AwaitBlock' ||
type === 'KeyBlock'
)) &&
!is_ignored(node, 'binding_property_non_reactive')
) {
validate_binding(context.state, node, expression);
}
const get = b.thunk(/** @type {Expression} */ (context.visit(expression)));
get = b.thunk(expression);
/** @type {Expression | undefined} */
let set = b.unthunk(
b.arrow(
[b.id('$$value')],
/** @type {Expression} */ (context.visit(b.assignment('=', expression, b.id('$$value'))))
)
);
/** @type {Expression | undefined} */
set = b.unthunk(
b.arrow(
[b.id('$$value')],
/** @type {Expression} */ (
context.visit(
b.assignment('=', /** @type {Pattern} */ (node.expression), b.id('$$value'))
)
)
)
);
if (get === set) {
set = undefined;
if (get === set) {
set = undefined;
}
}
/** @type {CallExpression} */
@ -162,7 +171,7 @@ export function BindDirective(node, context) {
break;
case 'this':
call = build_bind_this(expression, context.state.node, context);
call = build_bind_this(node.expression, context.state.node, context);
break;
case 'textContent':
@ -213,10 +222,7 @@ export function BindDirective(node, context) {
if (value !== undefined) {
group_getter = b.thunk(
b.block([
b.stmt(build_attribute_value(value, context).value),
b.return(/** @type {Expression} */ (context.visit(expression)))
])
b.block([b.stmt(build_attribute_value(value, context).value), b.return(expression)])
);
}
}

@ -23,6 +23,9 @@ export function ClassBody(node, context) {
/** @type {Map<string, StateField>} */
const private_state = new Map();
/** @type {Map<(MethodDefinition|PropertyDefinition)["key"], string>} */
const definition_names = new Map();
/** @type {string[]} */
const private_ids = [];
@ -34,9 +37,12 @@ export function ClassBody(node, context) {
definition.key.type === 'Literal')
) {
const type = definition.key.type;
const name = get_name(definition.key);
const name = get_name(definition.key, public_state);
if (!name) continue;
// we store the deconflicted name in the map so that we can access it later
definition_names.set(definition.key, name);
const is_private = type === 'PrivateIdentifier';
if (is_private) private_ids.push(name);
@ -96,7 +102,7 @@ export function ClassBody(node, context) {
definition.key.type === 'PrivateIdentifier' ||
definition.key.type === 'Literal')
) {
const name = get_name(definition.key);
const name = definition_names.get(definition.key);
if (!name) continue;
const is_private = definition.key.type === 'PrivateIdentifier';
@ -210,10 +216,20 @@ export function ClassBody(node, context) {
/**
* @param {Identifier | PrivateIdentifier | Literal} node
* @param {Map<string, StateField>} public_state
*/
function get_name(node) {
function get_name(node, public_state) {
if (node.type === 'Literal') {
return node.value?.toString().replace(regex_invalid_identifier_chars, '_');
let name = node.value?.toString().replace(regex_invalid_identifier_chars, '_');
// the above could generate conflicts because it has to generate a valid identifier
// so stuff like `0` and `1` or `state%` and `state^` will result in the same string
// so we have to de-conflict. We can only check `public_state` because private state
// can't have literal keys
while (name && public_state.has(name)) {
name = '_' + name;
}
return name;
} else {
return node.name;
}

@ -50,11 +50,6 @@ 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,
@ -62,9 +57,7 @@ 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)),
location && b.literal(location.line),
location && b.literal(location.column)
b.thunk(b.block(body))
)
)
);

@ -450,6 +450,11 @@ function setup_select_synchronization(value_binding, context) {
if (context.state.analysis.runes) return;
let bound = value_binding.expression;
if (bound.type === 'SequenceExpression') {
return;
}
while (bound.type === 'MemberExpression') {
bound = /** @type {Identifier | MemberExpression} */ (bound.object);
}
@ -484,10 +489,7 @@ function setup_select_synchronization(value_binding, context) {
b.call(
'$.template_effect',
b.thunk(
b.block([
b.stmt(/** @type {Expression} */ (context.visit(value_binding.expression))),
b.stmt(invalidator)
])
b.block([b.stmt(/** @type {Expression} */ (context.visit(bound))), b.stmt(invalidator)])
)
)
)

@ -1,5 +1,5 @@
/** @import { BlockStatement, Expression, ExpressionStatement, Identifier, MemberExpression, Property, Statement } from 'estree' */
/** @import { AST, TemplateNode } from '#compiler' */
/** @import { BlockStatement, Expression, ExpressionStatement, Identifier, MemberExpression, Pattern, Property, SequenceExpression, Statement } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../../types.js' */
import { dev, is_ignored } from '../../../../../state.js';
import { get_attribute_chunks, object } from '../../../../../utils/ast.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 = [];
@ -33,7 +35,7 @@ export function build_component(node, component_name, context, anchor = context.
}
};
/** @type {Record<string, TemplateNode[]>} */
/** @type {Record<string, AST.TemplateNode[]>} */
const children = {};
/** @type {Record<string, Expression[]>} */
@ -42,7 +44,7 @@ export function build_component(node, component_name, context, anchor = context.
/** @type {Property[]} */
const custom_css_props = [];
/** @type {Identifier | MemberExpression | null} */
/** @type {Identifier | MemberExpression | SequenceExpression | null} */
let bind_this = null;
/** @type {ExpressionStatement[]} */
@ -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();
}
}
@ -163,61 +174,89 @@ export function build_component(node, component_name, context, anchor = context.
} else if (attribute.type === 'BindDirective') {
const expression = /** @type {Expression} */ (context.visit(attribute.expression));
if (
dev &&
expression.type === 'MemberExpression' &&
context.state.analysis.runes &&
!is_ignored(node, 'binding_property_non_reactive')
) {
validate_binding(context.state, attribute, expression);
if (dev && attribute.name !== 'this' && attribute.expression.type !== 'SequenceExpression') {
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
)
)
);
}
}
if (attribute.name === 'this') {
bind_this = attribute.expression;
if (expression.type === 'SequenceExpression') {
if (attribute.name === 'this') {
bind_this = attribute.expression;
} else {
const [get, set] = expression.expressions;
const get_id = b.id(context.state.scope.generate('bind_get'));
const set_id = b.id(context.state.scope.generate('bind_set'));
context.state.init.push(b.var(get_id, get));
context.state.init.push(b.var(set_id, set));
push_prop(b.get(attribute.name, [b.return(b.call(get_id))]));
push_prop(b.set(attribute.name, [b.stmt(b.call(set_id, b.id('$$value')))]));
}
} else {
if (dev) {
const left = object(attribute.expression);
let binding;
if (left?.type === 'Identifier') {
binding = context.state.scope.get(left.name);
}
// Only run ownership addition on $state fields.
// Theoretically someone could create a `$state` while creating `$state.raw` or inside a `$derived.by`,
// but that feels so much of an edge case that it doesn't warrant a perf hit for the common case.
if (binding?.kind !== 'derived' && binding?.kind !== 'raw_state') {
binding_initializers.push(
b.stmt(
b.call(
b.id('$.add_owner_effect'),
b.thunk(expression),
b.id(component_name),
is_ignored(node, 'ownership_invalid_binding') && b.true
)
)
if (
dev &&
expression.type === 'MemberExpression' &&
context.state.analysis.runes &&
!is_ignored(node, 'binding_property_non_reactive')
) {
validate_binding(context.state, attribute, expression);
}
if (attribute.name === 'this') {
bind_this = attribute.expression;
} else {
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)]),
true
);
} else {
push_prop(b.get(attribute.name, [b.return(expression)]), true);
}
}
const is_store_sub =
attribute.expression.type === 'Identifier' &&
context.state.scope.get(attribute.expression.name)?.kind === 'store_sub';
const assignment = b.assignment(
'=',
/** @type {Pattern} */ (attribute.expression),
b.id('$$value')
);
if (is_store_sub) {
push_prop(
b.get(attribute.name, [b.stmt(b.call('$.mark_store_binding')), b.return(expression)])
b.set(attribute.name, [b.stmt(/** @type {Expression} */ (context.visit(assignment)))]),
true
);
} else {
push_prop(b.get(attribute.name, [b.return(expression)]));
}
const assignment = b.assignment('=', attribute.expression, b.id('$$value'));
push_prop(
b.set(attribute.name, [b.stmt(/** @type {Expression} */ (context.visit(assignment)))])
);
}
}
}
delayed_props.forEach((fn) => fn());
if (slot_scope_applies_to_itself) {
context.state.init.push(...lets);
}

@ -1,5 +1,5 @@
/** @import { Expression } from 'estree' */
/** @import { AST, ExpressionMetadata, SvelteNode } from '#compiler' */
/** @import { AST, ExpressionMetadata } from '#compiler' */
/** @import { ComponentContext } from '../../types' */
import { is_capture_event, is_passive_event } from '../../../../../../utils.js';
import { dev, locator } from '../../../../../state.js';
@ -68,7 +68,7 @@ export function visit_event_attribute(node, context) {
)
);
const type = /** @type {SvelteNode} */ (context.path.at(-1)).type;
const type = /** @type {AST.SvelteNode} */ (context.path.at(-1)).type;
if (type === 'SvelteDocument' || type === 'SvelteWindow' || type === 'SvelteBody') {
// These nodes are above the component tree, and its events should run parent first

@ -1,5 +1,5 @@
/** @import { Expression } from 'estree' */
/** @import { AST, SvelteNode } from '#compiler' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../../types' */
import { cannot_be_set_statically } from '../../../../../../utils.js';
import { is_event_attribute, is_text_attribute } from '../../../../../utils/ast.js';
@ -10,7 +10,7 @@ import { build_template_chunk, build_update } from './utils.js';
* Processes an array of template nodes, joining sibling text/expression nodes
* (e.g. `{a} b {c}`) into a single update function. Along the way it creates
* corresponding template node references these updates are applied to.
* @param {SvelteNode[]} nodes
* @param {AST.SvelteNode[]} nodes
* @param {(is_text: boolean) => Expression} initial
* @param {boolean} is_element
* @param {ComponentContext} context
@ -124,7 +124,7 @@ export function process_children(nodes, initial, is_element, { visit, state }) {
}
/**
* @param {SvelteNode} node
* @param {AST.SvelteNode} node
* @param {ComponentContext["state"]} state
*/
function is_static_element(node, state) {

@ -1,5 +1,5 @@
/** @import { Expression, ExpressionStatement, Identifier, MemberExpression, Statement, Super } from 'estree' */
/** @import { AST, SvelteNode } from '#compiler' */
/** @import { Expression, ExpressionStatement, Identifier, MemberExpression, SequenceExpression, Statement, Super } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentClientTransformState } from '../../types' */
import { walk } from 'zimmerframe';
import { object } from '../../../../../utils/ast.js';
@ -12,7 +12,7 @@ import { locator } from '../../../../../state.js';
/**
* @param {Array<AST.Text | AST.ExpressionTag>} values
* @param {(node: SvelteNode, state: any) => any} visit
* @param {(node: AST.SvelteNode, state: any) => any} visit
* @param {ComponentClientTransformState} state
* @returns {{ value: Expression, has_state: boolean, has_call: boolean }}
*/
@ -143,11 +143,16 @@ export function build_update_assignment(state, id, init, value, update) {
/**
* Serializes `bind:this` for components and elements.
* @param {Identifier | MemberExpression} expression
* @param {Identifier | MemberExpression | SequenceExpression} expression
* @param {Expression} value
* @param {import('zimmerframe').Context<SvelteNode, ComponentClientTransformState>} context
* @param {import('zimmerframe').Context<AST.SvelteNode, ComponentClientTransformState>} context
*/
export function build_bind_this(expression, value, { state, visit }) {
if (expression.type === 'SequenceExpression') {
const [get, set] = /** @type {SequenceExpression} */ (visit(expression)).expressions;
return b.call('$.bind_this', value, set, get);
}
/** @type {Identifier[]} */
const ids = [];
@ -224,6 +229,9 @@ export function build_bind_this(expression, value, { state, visit }) {
* @param {MemberExpression} expression
*/
export function validate_binding(state, binding, expression) {
if (binding.expression.type === 'SequenceExpression') {
return;
}
// If we are referencing a $store.foo then we don't need to add validation
const left = object(binding.expression);
const left_binding = left && state.scope.get(left.name);

@ -1,5 +1,5 @@
/** @import { Visitors } from 'zimmerframe' */
/** @import { ValidatedCompileOptions, Css } from '#compiler' */
/** @import { AST, ValidatedCompileOptions } from '#compiler' */
/** @import { ComponentAnalysis } from '../../types.js' */
import MagicString from 'magic-string';
import { walk } from 'zimmerframe';
@ -41,9 +41,9 @@ export function render_stylesheet(source, analysis, options) {
}
};
const ast = /** @type {Css.StyleSheet} */ (analysis.css.ast);
const ast = /** @type {AST.CSS.StyleSheet} */ (analysis.css.ast);
walk(/** @type {Css.Node} */ (ast), state, visitors);
walk(/** @type {AST.CSS.Node} */ (ast), state, visitors);
code.remove(0, ast.content.start);
code.remove(/** @type {number} */ (ast.content.end), source.length);
@ -71,14 +71,14 @@ export function render_stylesheet(source, analysis, options) {
return css;
}
/** @type {Visitors<Css.Node, State>} */
/** @type {Visitors<AST.CSS.Node, State>} */
const visitors = {
_: (node, context) => {
context.state.code.addSourcemapLocation(node.start);
context.state.code.addSourcemapLocation(node.end);
context.next();
},
Atrule(node, { state, next }) {
Atrule(node, { state, next, path }) {
if (is_keyframes_node(node)) {
let start = node.start + node.name.length + 1;
while (state.code.original[start] === ' ') start += 1;
@ -87,7 +87,7 @@ const visitors = {
if (node.prelude.startsWith('-global-')) {
state.code.remove(start, start + 8);
} else {
} else if (!is_in_global_block(path)) {
state.code.prependRight(start, `${state.hash}-`);
}
@ -134,7 +134,7 @@ const visitors = {
}
}
},
Rule(node, { state, next, visit }) {
Rule(node, { state, next, visit, path }) {
if (state.minify) {
remove_preceding_whitespace(node.start, state);
remove_preceding_whitespace(node.block.end - 1, state);
@ -142,7 +142,7 @@ const visitors = {
// keep empty rules in dev, because it's convenient to
// see them in devtools
if (!dev && is_empty(node)) {
if (!dev && is_empty(node, is_in_global_block(path))) {
if (state.minify) {
state.code.remove(node.start, node.end);
} else {
@ -154,7 +154,7 @@ const visitors = {
return;
}
if (!is_used(node)) {
if (!is_used(node) && !is_in_global_block(path)) {
if (state.minify) {
state.code.remove(node.start, node.end);
} else {
@ -182,20 +182,20 @@ const visitors = {
state.code.appendLeft(node.block.end, '*/');
}
// don't recurse into selector or body
// don't recurse into selectors but visit the body
visit(node.block);
return;
}
// don't recurse into body
visit(node.prelude);
return;
}
next();
},
SelectorList(node, { state, next, path }) {
// Only add comments if we're not inside a complex selector that itself is unused
if (!path.find((n) => n.type === 'ComplexSelector' && !n.metadata.used)) {
// Only add comments if we're not inside a complex selector that itself is unused or a global block
if (
!is_in_global_block(path) &&
!path.find((n) => n.type === 'ComplexSelector' && !n.metadata.used)
) {
const children = node.children;
let pruning = false;
let prune_start = children[0].start;
@ -259,7 +259,7 @@ const visitors = {
if (parent?.type === 'Rule') {
specificity = { bumped: false };
/** @type {Css.Rule | null} */
/** @type {AST.CSS.Rule | null} */
let rule = parent.metadata.parent_rule;
while (rule) {
@ -278,7 +278,7 @@ const visitors = {
for (const relative_selector of node.children) {
if (relative_selector.metadata.is_global) {
const global = /** @type {Css.PseudoClassSelector} */ (relative_selector.selectors[0]);
const global = /** @type {AST.CSS.PseudoClassSelector} */ (relative_selector.selectors[0]);
remove_global_pseudo_class(global, relative_selector.combinator, context.state);
if (
@ -360,8 +360,16 @@ const visitors = {
};
/**
* @param {Css.PseudoClassSelector} selector
* @param {Css.Combinator | null} combinator
*
* @param {Array<AST.CSS.Node>} path
*/
function is_in_global_block(path) {
return path.some((node) => node.type === 'Rule' && node.metadata.is_global_block);
}
/**
* @param {AST.CSS.PseudoClassSelector} selector
* @param {AST.CSS.Combinator | null} combinator
* @param {State} state
*/
function remove_global_pseudo_class(selector, combinator, state) {
@ -390,8 +398,11 @@ function remove_preceding_whitespace(end, state) {
if (start < end) state.code.remove(start, end);
}
/** @param {Css.Rule} rule */
function is_empty(rule) {
/**
* @param {AST.CSS.Rule} rule
* @param {boolean} is_in_global_block
*/
function is_empty(rule, is_in_global_block) {
if (rule.metadata.is_global_block) {
return rule.block.children.length === 0;
}
@ -402,7 +413,9 @@ function is_empty(rule) {
}
if (child.type === 'Rule') {
if (is_used(child) && !is_empty(child)) return false;
if ((is_used(child) || is_in_global_block) && !is_empty(child, is_in_global_block)) {
return false;
}
}
if (child.type === 'Atrule') {
@ -413,14 +426,14 @@ function is_empty(rule) {
return true;
}
/** @param {Css.Rule} rule */
/** @param {AST.CSS.Rule} rule */
function is_used(rule) {
return rule.prelude.children.some((selector) => selector.metadata.used);
}
/**
*
* @param {Css.Rule} node
* @param {AST.CSS.Rule} node
* @param {MagicString} code
*/
function escape_comment_close(node, code) {

@ -1,5 +1,5 @@
/** @import { Program, Property, Statement, VariableDeclarator } from 'estree' */
/** @import { SvelteNode, ValidatedCompileOptions, ValidatedModuleCompileOptions } from '#compiler' */
/** @import { AST, ValidatedCompileOptions, ValidatedModuleCompileOptions } from '#compiler' */
/** @import { ComponentServerTransformState, ComponentVisitors, ServerTransformState, Visitors } from './types.js' */
/** @import { Analysis, ComponentAnalysis } from '../../types.js' */
import { walk } from 'zimmerframe';
@ -104,12 +104,12 @@ export function server_component(analysis, options) {
};
const module = /** @type {Program} */ (
walk(/** @type {SvelteNode} */ (analysis.module.ast), state, global_visitors)
walk(/** @type {AST.SvelteNode} */ (analysis.module.ast), state, global_visitors)
);
const instance = /** @type {Program} */ (
walk(
/** @type {SvelteNode} */ (analysis.instance.ast),
/** @type {AST.SvelteNode} */ (analysis.instance.ast),
{ ...state, scopes: analysis.instance.scopes },
{
...global_visitors,
@ -130,7 +130,7 @@ export function server_component(analysis, options) {
const template = /** @type {Program} */ (
walk(
/** @type {SvelteNode} */ (analysis.template.ast),
/** @type {AST.SvelteNode} */ (analysis.template.ast),
{ ...state, scopes: analysis.template.scopes },
// @ts-expect-error don't know, don't care
{ ...global_visitors, ...template_visitors }
@ -394,7 +394,7 @@ export function server_module(analysis, options) {
};
const module = /** @type {Program} */ (
walk(/** @type {SvelteNode} */ (analysis.module.ast), state, global_visitors)
walk(/** @type {AST.SvelteNode} */ (analysis.module.ast), state, global_visitors)
);
return {

@ -1,5 +1,5 @@
import type { Expression, Statement, ModuleDeclaration, LabeledStatement } from 'estree';
import type { Namespace, SvelteNode, ValidatedCompileOptions } from '#compiler';
import type { AST, Namespace, ValidatedCompileOptions } from '#compiler';
import type { TransformState } from '../types.js';
import type { ComponentAnalysis } from '../../types.js';
import type { StateField } from '../client/types.js';
@ -25,14 +25,14 @@ export interface ComponentServerTransformState extends ServerTransformState {
readonly skip_hydration_boundaries: boolean;
}
export type Context = import('zimmerframe').Context<SvelteNode, ServerTransformState>;
export type Visitors = import('zimmerframe').Visitors<SvelteNode, ServerTransformState>;
export type Context = import('zimmerframe').Context<AST.SvelteNode, ServerTransformState>;
export type Visitors = import('zimmerframe').Visitors<AST.SvelteNode, ServerTransformState>;
export type ComponentContext = import('zimmerframe').Context<
SvelteNode,
AST.SvelteNode,
ComponentServerTransformState
>;
export type ComponentVisitors = import('zimmerframe').Visitors<
SvelteNode,
AST.SvelteNode,
ComponentServerTransformState
>;

@ -1,5 +1,5 @@
/** @import { AssignmentExpression, AssignmentOperator, Expression, Pattern } from 'estree' */
/** @import { SvelteNode } from '#compiler' */
/** @import { AST } from '#compiler' */
/** @import { Context, ServerTransformState } from '../types.js' */
import * as b from '../../../../utils/builders.js';
import { build_assignment_value } from '../../../../utils/ast.js';
@ -18,7 +18,7 @@ export function AssignmentExpression(node, context) {
* @param {AssignmentOperator} operator
* @param {Pattern} left
* @param {Expression} right
* @param {import('zimmerframe').Context<SvelteNode, ServerTransformState>} context
* @param {import('zimmerframe').Context<AST.SvelteNode, ServerTransformState>} context
* @returns {Expression | null}
*/
function build_assignment(operator, left, right, context) {

@ -1,5 +1,5 @@
/** @import { BlockStatement, Expression, Pattern, Property, Statement } from 'estree' */
/** @import { AST, TemplateNode } from '#compiler' */
/** @import { BlockStatement, Expression, Pattern, Property, SequenceExpression, Statement } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../../types.js' */
import { empty_comment, build_attribute_value } from './utils.js';
import * as b from '../../../../../utils/builders.js';
@ -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 = [];
@ -29,7 +31,7 @@ export function build_inline_component(node, expression, context) {
scope: node.metadata.scopes.default
};
/** @type {Record<string, TemplateNode[]>} */
/** @type {Record<string, AST.TemplateNode[]>} */
const children = {};
/**
@ -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,25 +92,43 @@ 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
push_prop(
b.get(attribute.name, [
b.return(/** @type {Expression} */ (context.visit(attribute.expression)))
])
);
push_prop(
b.set(attribute.name, [
b.stmt(
/** @type {Expression} */ (
context.visit(b.assignment('=', attribute.expression, b.id('$$value')))
)
),
b.stmt(b.assignment('=', b.id('$$settled'), b.false))
])
);
if (attribute.expression.type === 'SequenceExpression') {
const [get, set] = /** @type {SequenceExpression} */ (context.visit(attribute.expression))
.expressions;
const get_id = b.id(context.state.scope.generate('bind_get'));
const set_id = b.id(context.state.scope.generate('bind_set'));
context.state.init.push(b.var(get_id, get));
context.state.init.push(b.var(set_id, set));
push_prop(b.get(attribute.name, [b.return(b.call(get_id))]));
push_prop(b.set(attribute.name, [b.stmt(b.call(set_id, b.id('$$value')))]));
} else {
// 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, [
b.stmt(
/** @type {Expression} */ (
context.visit(b.assignment('=', attribute.expression, b.id('$$value')))
)
),
b.stmt(b.assignment('=', b.id('$$settled'), b.false))
]),
true
);
}
}
}
delayed_props.forEach((fn) => fn());
/** @type {Statement[]} */
const snippet_declarations = [];

@ -1,5 +1,5 @@
/** @import { Expression, Literal } from 'estree' */
/** @import { AST, Namespace, SvelteNode } from '#compiler' */
/** @import { AST, Namespace } from '#compiler' */
/** @import { ComponentContext, ComponentServerTransformState } from '../../types.js' */
import {
get_attribute_chunks,
@ -31,7 +31,7 @@ const WHITESPACE_INSENSITIVE_ATTRIBUTES = ['class', 'style'];
* Writes the output to the template output. Some elements may have attributes on them that require the
* their output to be the child content instead. In this case, an object is returned.
* @param {AST.RegularElement | AST.SvelteElement} node
* @param {import('zimmerframe').Context<SvelteNode, ComponentServerTransformState>} context
* @param {import('zimmerframe').Context<AST.SvelteNode, ComponentServerTransformState>} context
*/
export function build_element_attributes(node, context) {
/** @type {Array<AST.Attribute | AST.SpreadAttribute>} */
@ -110,14 +110,17 @@ export function build_element_attributes(node, context) {
const binding = binding_properties[attribute.name];
if (binding?.omit_in_ssr) continue;
let expression = /** @type {Expression} */ (context.visit(attribute.expression));
if (expression.type === 'SequenceExpression') {
expression = b.call(expression.expressions[0]);
}
if (is_content_editable_binding(attribute.name)) {
content = /** @type {Expression} */ (context.visit(attribute.expression));
content = expression;
} else if (attribute.name === 'value' && node.name === 'textarea') {
content = b.call(
'$.escape',
/** @type {Expression} */ (context.visit(attribute.expression))
);
} else if (attribute.name === 'group') {
content = b.call('$.escape', expression);
} else if (attribute.name === 'group' && attribute.expression.type !== 'SequenceExpression') {
const value_attribute = /** @type {AST.Attribute | undefined} */ (
node.attributes.find((attr) => attr.type === 'Attribute' && attr.name === 'value')
);
@ -130,6 +133,7 @@ export function build_element_attributes(node, context) {
is_text_attribute(attr) &&
attr.value[0].data === 'checkbox'
);
attributes.push(
create_attribute('checked', -1, -1, [
{
@ -159,7 +163,7 @@ export function build_element_attributes(node, context) {
type: 'ExpressionTag',
start: -1,
end: -1,
expression: attribute.expression,
expression,
metadata: {
expression: create_expression_metadata()
}

@ -1,5 +1,5 @@
/** @import { AssignmentOperator, Expression, Identifier, Node, Statement, TemplateElement } from 'estree' */
/** @import { AST, SvelteNode } from '#compiler' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext, ServerTransformState } from '../../types.js' */
import { escape_html } from '../../../../../../escaping.js';
@ -24,7 +24,7 @@ export const empty_comment = b.literal(EMPTY_COMMENT);
/**
* Processes an array of template nodes, joining sibling text/expression nodes and
* recursing into child nodes.
* @param {Array<SvelteNode>} nodes
* @param {Array<AST.SvelteNode>} nodes
* @param {ComponentContext} context
*/
export function process_children(nodes, { visit, state }) {

@ -1,10 +1,10 @@
import type { Scope } from '../scope.js';
import type { SvelteNode, ValidatedModuleCompileOptions } from '#compiler';
import type { AST, ValidatedModuleCompileOptions } from '#compiler';
import type { Analysis } from '../types.js';
export interface TransformState {
readonly analysis: Analysis;
readonly options: ValidatedModuleCompileOptions;
readonly scope: Scope;
readonly scopes: Map<SvelteNode, Scope>;
readonly scopes: Map<AST.SvelteNode, Scope>;
}

@ -1,6 +1,6 @@
/** @import { Context } from 'zimmerframe' */
/** @import { TransformState } from './types.js' */
/** @import { AST, Binding, Namespace, SvelteNode, ValidatedCompileOptions } from '#compiler' */
/** @import { AST, Binding, Namespace, ValidatedCompileOptions } from '#compiler' */
/** @import { Node, Expression, CallExpression } from 'estree' */
import {
regex_ends_with_whitespaces,
@ -33,7 +33,7 @@ export function is_hoisted_function(node) {
/**
* Match Svelte 4 behaviour by sorting ConstTag nodes in topological order
* @param {SvelteNode[]} nodes
* @param {AST.SvelteNode[]} nodes
* @param {TransformState} state
*/
function sort_const_tags(nodes, state) {
@ -133,9 +133,9 @@ function sort_const_tags(nodes, state) {
* unless it's whitespace-only, in which case collapse to a single whitespace for all cases
* except when it's children of certain elements where we know ignore whitespace (like td/option/head),
* in which case we remove it entirely
* @param {SvelteNode} parent
* @param {SvelteNode[]} nodes
* @param {SvelteNode[]} path
* @param {AST.SvelteNode} parent
* @param {AST.SvelteNode[]} nodes
* @param {AST.SvelteNode[]} path
* @param {Namespace} namespace
* @param {TransformState & { options: ValidatedCompileOptions }} state
* @param {boolean} preserve_whitespace
@ -157,10 +157,10 @@ export function clean_nodes(
nodes = sort_const_tags(nodes, state);
}
/** @type {SvelteNode[]} */
/** @type {AST.SvelteNode[]} */
const hoisted = [];
/** @type {SvelteNode[]} */
/** @type {AST.SvelteNode[]} */
const regular = [];
for (const node of nodes) {
@ -316,8 +316,8 @@ export function clean_nodes(
/**
* Infers the namespace for the children of a node that should be used when creating the `$.template(...)`.
* @param {Namespace} namespace
* @param {SvelteNode} parent
* @param {SvelteNode[]} nodes
* @param {AST.SvelteNode} parent
* @param {AST.SvelteNode[]} nodes
*/
export function infer_namespace(namespace, parent, nodes) {
if (parent.type === 'RegularElement' && parent.name === 'foreignObject') {
@ -347,14 +347,31 @@ export function infer_namespace(namespace, parent, nodes) {
}
}
return namespace;
/** @type {Namespace | null} */
let new_namespace = null;
// Check the elements within the fragment and look for consistent namespaces.
// If we have no namespaces or they are mixed, then fallback to existing namespace
for (const node of nodes) {
if (node.type !== 'RegularElement') continue;
if (node.metadata.mathml) {
new_namespace = new_namespace === null || new_namespace === 'mathml' ? 'mathml' : 'html';
} else if (node.metadata.svg) {
new_namespace = new_namespace === null || new_namespace === 'svg' ? 'svg' : 'html';
} else {
return 'html';
}
}
return new_namespace ?? namespace;
}
/**
* Heuristic: Keep current namespace, unless we find a regular element,
* in which case we always want html, or we only find svg nodes,
* in which case we assume svg.
* @param {SvelteNode[]} nodes
* @param {AST.SvelteNode[]} nodes
* @param {Namespace | 'keep' | 'maybe_html'} namespace
*/
function check_nodes_for_namespace(nodes, namespace) {

@ -1,4 +1,4 @@
/** @import { Css } from '#compiler' */
/** @import { AST } from '#compiler' */
const regex_css_browser_prefix = /^-((webkit)|(moz)|(o)|(ms))-/;
export const regex_css_name_boundary = /^[\s,;}]$/;
@ -10,5 +10,5 @@ export function remove_css_prefix(name) {
return name.replace(regex_css_browser_prefix, '');
}
/** @param {Css.Atrule} node */
/** @param {AST.CSS.Atrule} node */
export const is_keyframes_node = (node) => remove_css_prefix(node.name) === 'keyframes';

@ -1,4 +1,4 @@
/** @import { AST, ExpressionMetadata, SvelteNode } from '#compiler' */
/** @import { AST, ExpressionMetadata } from '#compiler' */
/**
* All nodes that can appear elsewhere than the top level, have attributes and can contain children
*/
@ -14,7 +14,7 @@ const element_nodes = [
/**
* Returns true for all nodes that can appear elsewhere than the top level, have attributes and can contain children
* @param {SvelteNode} node
* @param {AST.SvelteNode} node
* @returns {node is AST.Component | AST.RegularElement | AST.SlotElement | AST.SvelteComponent | AST.SvelteElement | AST.SvelteFragment | AST.SvelteSelf}
*/
export function is_element_node(node) {

@ -1,6 +1,6 @@
/** @import { ClassDeclaration, Expression, FunctionDeclaration, Identifier, ImportDeclaration, MemberExpression, Node, Pattern, VariableDeclarator } from 'estree' */
/** @import { Context, Visitor } from 'zimmerframe' */
/** @import { AST, Binding, DeclarationKind, ElementLike, SvelteNode } from '#compiler' */
/** @import { AST, Binding, DeclarationKind } from '#compiler' */
import is_reference from 'is-reference';
import { walk } from 'zimmerframe';
import { create_expression_metadata } from './nodes.js';
@ -48,7 +48,7 @@ export class Scope {
/**
* A set of all the names referenced with this scope
* useful for generating unique names
* @type {Map<string, { node: Identifier; path: SvelteNode[] }[]>}
* @type {Map<string, { node: Identifier; path: AST.SvelteNode[] }[]>}
*/
references = new Map();
@ -185,7 +185,7 @@ export class Scope {
/**
* @param {Identifier} node
* @param {SvelteNode[]} path
* @param {AST.SvelteNode[]} path
*/
reference(node, path) {
path = [...path]; // ensure that mutations to path afterwards don't affect this reference
@ -231,7 +231,7 @@ export class ScopeRoot {
}
/**
* @param {SvelteNode} ast
* @param {AST.SvelteNode} ast
* @param {ScopeRoot} root
* @param {boolean} allow_reactive_declarations
* @param {Scope | null} parent
@ -241,7 +241,7 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
/**
* A map of node->associated scope. A node appearing in this map does not necessarily mean that it created a scope
* @type {Map<SvelteNode, Scope>}
* @type {Map<AST.SvelteNode, Scope>}
*/
const scopes = new Map();
const scope = new Scope(root, parent, false);
@ -250,7 +250,7 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
/** @type {State} */
const state = { scope };
/** @type {[Scope, { node: Identifier; path: SvelteNode[] }][]} */
/** @type {[Scope, { node: Identifier; path: AST.SvelteNode[] }][]} */
const references = [];
/** @type {[Scope, Pattern | MemberExpression][]} */
@ -275,7 +275,7 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
}
/**
* @type {Visitor<Node, State, SvelteNode>}
* @type {Visitor<Node, State, AST.SvelteNode>}
*/
const create_block_scope = (node, { state, next }) => {
const scope = state.scope.child(true);
@ -285,7 +285,7 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
};
/**
* @type {Visitor<ElementLike, State, SvelteNode>}
* @type {Visitor<AST.ElementLike, State, AST.SvelteNode>}
*/
const SvelteFragment = (node, { state, next }) => {
const scope = state.scope.child();
@ -294,7 +294,7 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
};
/**
* @type {Visitor<AST.Component | AST.SvelteComponent | AST.SvelteSelf, State, SvelteNode>}
* @type {Visitor<AST.Component | AST.SvelteComponent | AST.SvelteSelf, State, AST.SvelteNode>}
*/
const Component = (node, context) => {
node.metadata.scopes = {
@ -335,7 +335,7 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
};
/**
* @type {Visitor<AST.AnimateDirective | AST.TransitionDirective | AST.UseDirective, State, SvelteNode>}
* @type {Visitor<AST.AnimateDirective | AST.TransitionDirective | AST.UseDirective, State, AST.SvelteNode>}
*/
const SvelteDirective = (node, { state, path, visit }) => {
state.scope.reference(b.id(node.name.split('.')[0]), path);
@ -634,12 +634,8 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
SnippetBlock(node, context) {
const state = context.state;
// Special-case for root-level snippets: they become part of the instance scope
const is_top_level = !context.path.at(-2);
let scope = state.scope;
if (is_top_level) {
scope = /** @type {Scope} */ (parent);
}
scope.declare(node.expression, 'normal', 'function', node);
const child_scope = state.scope.child();
@ -723,9 +719,9 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
}
/**
* @template {{ scope: Scope, scopes: Map<SvelteNode, Scope> }} State
* @param {SvelteNode} node
* @param {Context<SvelteNode, State>} context
* @template {{ scope: Scope, scopes: Map<AST.SvelteNode, Scope> }} State
* @param {AST.SvelteNode} node
* @param {Context<AST.SvelteNode, State>} context
*/
export function set_scope(node, { next, state }) {
const scope = state.scopes.get(node);

@ -1,17 +1,17 @@
import type { AST, Binding, Css, SvelteNode } from '#compiler';
import type { AST, Binding } from '#compiler';
import type { Identifier, LabeledStatement, Node, Program } from 'estree';
import type { Scope, ScopeRoot } from './scope.js';
export interface Js {
ast: Program;
scope: Scope;
scopes: Map<SvelteNode, Scope>;
scopes: Map<AST.SvelteNode, Scope>;
}
export interface Template {
ast: AST.Fragment;
scope: Scope;
scopes: Map<SvelteNode, Scope>;
scopes: Map<AST.SvelteNode, Scope>;
}
export interface ReactiveStatement {
@ -66,7 +66,7 @@ export interface ComponentAnalysis extends Analysis {
binding_groups: Map<[key: string, bindings: Array<Binding | null>], Identifier>;
slot_names: Map<string, AST.SlotElement>;
css: {
ast: Css.StyleSheet | null;
ast: AST.CSS.StyleSheet | null;
hash: string;
keyframes: string[];
};

@ -1,5 +1,5 @@
/** @import { CompileOptions, SvelteNode } from './types' */
/** @import { Warning } from '#compiler' */
/** @import { CompileOptions } from './types' */
/** @import { AST, Warning } from '#compiler' */
import { getLocator } from 'locate-character';
/** @typedef {{ start?: number, end?: number }} NodeLike */
@ -41,7 +41,7 @@ export let ignore_stack = [];
* For each node the list of warnings that should be ignored for that node.
* Exists in addition to `ignore_stack` because not all warnings are emitted
* while the stack is being built.
* @type {Map<SvelteNode | NodeLike, Set<string>[]>}
* @type {Map<AST.SvelteNode | NodeLike, Set<string>[]>}
*/
export let ignore_map = new Map();
@ -66,7 +66,7 @@ export function reset_warning_filter(fn = () => true) {
}
/**
* @param {SvelteNode | NodeLike} node
* @param {AST.SvelteNode | NodeLike} node
* @param {import('../constants.js').IGNORABLE_RUNTIME_WARNINGS[number]} code
* @returns
*/

@ -1,6 +1,6 @@
import type { AST } from '#compiler';
export namespace Css {
export namespace _CSS {
export interface BaseNode {
start: number;
end: number;

@ -7,8 +7,7 @@ import type {
} from 'estree';
import type { SourceMap } from 'magic-string';
import type { Scope } from '../phases/scope.js';
import type { Css } from './css.js';
import type { AST, Namespace, SvelteNode } from './template.js';
import type { AST, Namespace } from './template.js';
import type { ICompileDiagnostic } from '../utils/compile_diagnostic.js';
/** The return value of `compile` from `svelte/compiler` */
@ -295,7 +294,7 @@ export interface Binding {
| AST.EachBlock
| AST.SnippetBlock;
is_called: boolean;
references: { node: Identifier; path: SvelteNode[] }[];
references: { node: Identifier; path: AST.SvelteNode[] }[];
mutated: boolean;
reassigned: boolean;
/** `true` if mutated _or_ reassigned */
@ -322,7 +321,6 @@ export interface ExpressionMetadata {
}
export * from './template.js';
export { Css };
// TODO this chain is a bit weird
export { ReactiveStatement } from '../phases/types.js';

@ -1,4 +1,4 @@
import type { AST, Css } from '#compiler';
import type { AST } from '#compiler';
import type {
ArrayExpression,
AssignmentExpression,
@ -6,7 +6,8 @@ import type {
Identifier,
MemberExpression,
ObjectExpression,
Pattern
Pattern,
SequenceExpression
} from 'estree';
interface BaseNode {
@ -49,7 +50,7 @@ export interface LegacyBinding extends BaseNode {
/** The 'x' in `bind:x` */
name: string;
/** The y in `bind:x={y}` */
expression: Identifier | MemberExpression;
expression: Identifier | MemberExpression | SequenceExpression;
}
export interface LegacyBody extends BaseElement {
@ -259,7 +260,7 @@ export interface LegacyStyle extends BaseNode {
export interface LegacySelector extends BaseNode {
type: 'Selector';
children: Array<Css.Combinator | Css.SimpleSelector>;
children: Array<AST.CSS.Combinator | AST.CSS.SimpleSelector>;
}
export type LegacyCssNode = LegacyStyle | LegacySelector;

@ -1,4 +1,4 @@
import type { Binding, Css, ExpressionMetadata } from '#compiler';
import type { Binding, ExpressionMetadata } from '#compiler';
import type {
ArrayExpression,
ArrowFunctionExpression,
@ -14,9 +14,11 @@ import type {
Pattern,
Program,
ChainExpression,
SimpleCallExpression
SimpleCallExpression,
SequenceExpression
} from 'estree';
import type { Scope } from '../phases/scope';
import type { _CSS } from './css';
/**
* - `html` the default, for e.g. `<div>` or `<span>`
@ -65,7 +67,7 @@ export namespace AST {
options: SvelteOptions | null;
fragment: Fragment;
/** The parsed `<style>` element, if exists */
css: Css.StyleSheet | null;
css: AST.CSS.StyleSheet | null;
/** The parsed `<script>` element, if exists */
instance: Script | null;
/** The parsed `<script module>` element, if exists */
@ -187,7 +189,7 @@ export namespace AST {
/** The 'x' in `bind:x` */
name: string;
/** The y in `bind:x={y}` */
expression: Identifier | MemberExpression;
expression: Identifier | MemberExpression | SequenceExpression;
/** @internal */
metadata: {
binding_group_name: Identifier;
@ -498,50 +500,60 @@ export namespace AST {
content: Program;
attributes: Attribute[];
}
}
type Tag = AST.ExpressionTag | AST.HtmlTag | AST.ConstTag | AST.DebugTag | AST.RenderTag;
export type Directive =
| AST.AnimateDirective
| AST.BindDirective
| AST.ClassDirective
| AST.LetDirective
| AST.OnDirective
| AST.StyleDirective
| AST.TransitionDirective
| AST.UseDirective;
export type Block = AST.EachBlock | AST.IfBlock | AST.AwaitBlock | AST.KeyBlock | AST.SnippetBlock;
export type ElementLike =
| AST.Component
| AST.TitleElement
| AST.SlotElement
| AST.RegularElement
| AST.SvelteBody
| AST.SvelteComponent
| AST.SvelteDocument
| AST.SvelteElement
| AST.SvelteFragment
| AST.SvelteHead
| AST.SvelteOptionsRaw
| AST.SvelteSelf
| AST.SvelteWindow
| AST.SvelteBoundary;
export type TemplateNode =
| AST.Root
| AST.Text
| Tag
| ElementLike
| AST.Attribute
| AST.SpreadAttribute
| Directive
| AST.Comment
| Block;
export type SvelteNode = Node | TemplateNode | AST.Fragment | Css.Node;
export type AttributeLike = Attribute | SpreadAttribute | Directive;
export type Directive =
| AST.AnimateDirective
| AST.BindDirective
| AST.ClassDirective
| AST.LetDirective
| AST.OnDirective
| AST.StyleDirective
| AST.TransitionDirective
| AST.UseDirective;
export type Block =
| AST.EachBlock
| AST.IfBlock
| AST.AwaitBlock
| AST.KeyBlock
| AST.SnippetBlock;
export type ElementLike =
| AST.Component
| AST.TitleElement
| AST.SlotElement
| AST.RegularElement
| AST.SvelteBody
| AST.SvelteBoundary
| AST.SvelteComponent
| AST.SvelteDocument
| AST.SvelteElement
| AST.SvelteFragment
| AST.SvelteHead
| AST.SvelteOptionsRaw
| AST.SvelteSelf
| AST.SvelteWindow
| AST.SvelteBoundary;
export type Tag = AST.ExpressionTag | AST.HtmlTag | AST.ConstTag | AST.DebugTag | AST.RenderTag;
export type TemplateNode =
| AST.Root
| AST.Text
| Tag
| ElementLike
| AST.Attribute
| AST.SpreadAttribute
| Directive
| AST.Comment
| Block;
export type SvelteNode = Node | TemplateNode | AST.Fragment | _CSS.Node;
export type { _CSS as CSS };
}
declare module 'estree' {
export interface BaseNode {

@ -1,4 +1,4 @@
/** @import { AST, SvelteNode } from '#compiler' */
/** @import { AST } from '#compiler' */
/** @import * as ESTree from 'estree' */
import { walk } from 'zimmerframe';
import * as b from '../utils/builders.js';
@ -393,7 +393,7 @@ function _extract_paths(assignments = [], param, expression, update_expression,
* Like `path.at(x)`, but skips over `TSNonNullExpression` and `TSAsExpression` nodes and eases assertions a bit
* by removing the `| undefined` from the resulting type.
*
* @template {SvelteNode} T
* @template {AST.SvelteNode} T
* @param {T[]} path
* @param {number} at
*/

@ -1,9 +1,9 @@
/** @import { SvelteNode } from '#compiler' */
/** @import { AST } from '#compiler' */
import { is_element_node } from '../phases/nodes.js';
import { is_text_attribute } from './ast.js';
/**
* @param {SvelteNode} node
* @param {AST.SvelteNode} node
*/
export function determine_slot(node) {
if (!is_element_node(node)) return null;

@ -129,7 +129,7 @@ export const codes = [
* @param {null | NodeLike} node
*/
export function a11y_accesskey(node) {
w(node, "a11y_accesskey", "Avoid using accesskey");
w(node, "a11y_accesskey", `Avoid using accesskey\nhttps://svelte.dev/e/a11y_accesskey`);
}
/**
@ -137,7 +137,7 @@ export function a11y_accesskey(node) {
* @param {null | NodeLike} node
*/
export function a11y_aria_activedescendant_has_tabindex(node) {
w(node, "a11y_aria_activedescendant_has_tabindex", "An element with an aria-activedescendant attribute should have a tabindex value");
w(node, "a11y_aria_activedescendant_has_tabindex", `An element with an aria-activedescendant attribute should have a tabindex value\nhttps://svelte.dev/e/a11y_aria_activedescendant_has_tabindex`);
}
/**
@ -146,7 +146,7 @@ export function a11y_aria_activedescendant_has_tabindex(node) {
* @param {string} name
*/
export function a11y_aria_attributes(node, name) {
w(node, "a11y_aria_attributes", `\`<${name}>\` should not have aria-* attributes`);
w(node, "a11y_aria_attributes", `\`<${name}>\` should not have aria-* attributes\nhttps://svelte.dev/e/a11y_aria_attributes`);
}
/**
@ -156,7 +156,7 @@ export function a11y_aria_attributes(node, name) {
* @param {string} type
*/
export function a11y_autocomplete_valid(node, value, type) {
w(node, "a11y_autocomplete_valid", `'${value}' is an invalid value for 'autocomplete' on \`<input type="${type}">\``);
w(node, "a11y_autocomplete_valid", `'${value}' is an invalid value for 'autocomplete' on \`<input type="${type}">\`\nhttps://svelte.dev/e/a11y_autocomplete_valid`);
}
/**
@ -164,7 +164,7 @@ export function a11y_autocomplete_valid(node, value, type) {
* @param {null | NodeLike} node
*/
export function a11y_autofocus(node) {
w(node, "a11y_autofocus", "Avoid using autofocus");
w(node, "a11y_autofocus", `Avoid using autofocus\nhttps://svelte.dev/e/a11y_autofocus`);
}
/**
@ -172,7 +172,7 @@ export function a11y_autofocus(node) {
* @param {null | NodeLike} node
*/
export function a11y_click_events_have_key_events(node) {
w(node, "a11y_click_events_have_key_events", "Visible, non-interactive elements with a click event must be accompanied by a keyboard event handler. Consider whether an interactive element such as `<button type=\"button\">` or `<a>` might be more appropriate. See https://svelte.dev/docs/accessibility-warnings#a11y-click-events-have-key-events for more details");
w(node, "a11y_click_events_have_key_events", `Visible, non-interactive elements with a click event must be accompanied by a keyboard event handler. Consider whether an interactive element such as \`<button type="button">\` or \`<a>\` might be more appropriate. See https://svelte.dev/docs/accessibility-warnings#a11y-click-events-have-key-events for more details\nhttps://svelte.dev/e/a11y_click_events_have_key_events`);
}
/**
@ -180,7 +180,7 @@ export function a11y_click_events_have_key_events(node) {
* @param {null | NodeLike} node
*/
export function a11y_consider_explicit_label(node) {
w(node, "a11y_consider_explicit_label", "Buttons and links should either contain text or have an `aria-label` or `aria-labelledby` attribute");
w(node, "a11y_consider_explicit_label", `Buttons and links should either contain text or have an \`aria-label\` or \`aria-labelledby\` attribute\nhttps://svelte.dev/e/a11y_consider_explicit_label`);
}
/**
@ -189,7 +189,7 @@ export function a11y_consider_explicit_label(node) {
* @param {string} name
*/
export function a11y_distracting_elements(node, name) {
w(node, "a11y_distracting_elements", `Avoid \`<${name}>\` elements`);
w(node, "a11y_distracting_elements", `Avoid \`<${name}>\` elements\nhttps://svelte.dev/e/a11y_distracting_elements`);
}
/**
@ -197,7 +197,7 @@ export function a11y_distracting_elements(node, name) {
* @param {null | NodeLike} node
*/
export function a11y_figcaption_index(node) {
w(node, "a11y_figcaption_index", "`<figcaption>` must be first or last child of `<figure>`");
w(node, "a11y_figcaption_index", `\`<figcaption>\` must be first or last child of \`<figure>\`\nhttps://svelte.dev/e/a11y_figcaption_index`);
}
/**
@ -205,7 +205,7 @@ export function a11y_figcaption_index(node) {
* @param {null | NodeLike} node
*/
export function a11y_figcaption_parent(node) {
w(node, "a11y_figcaption_parent", "`<figcaption>` must be an immediate child of `<figure>`");
w(node, "a11y_figcaption_parent", `\`<figcaption>\` must be an immediate child of \`<figure>\`\nhttps://svelte.dev/e/a11y_figcaption_parent`);
}
/**
@ -214,7 +214,7 @@ export function a11y_figcaption_parent(node) {
* @param {string} name
*/
export function a11y_hidden(node, name) {
w(node, "a11y_hidden", `\`<${name}>\` element should not be hidden`);
w(node, "a11y_hidden", `\`<${name}>\` element should not be hidden\nhttps://svelte.dev/e/a11y_hidden`);
}
/**
@ -222,7 +222,7 @@ export function a11y_hidden(node, name) {
* @param {null | NodeLike} node
*/
export function a11y_img_redundant_alt(node) {
w(node, "a11y_img_redundant_alt", "Screenreaders already announce `<img>` elements as an image");
w(node, "a11y_img_redundant_alt", `Screenreaders already announce \`<img>\` elements as an image\nhttps://svelte.dev/e/a11y_img_redundant_alt`);
}
/**
@ -232,7 +232,7 @@ export function a11y_img_redundant_alt(node) {
* @param {string} type
*/
export function a11y_incorrect_aria_attribute_type(node, attribute, type) {
w(node, "a11y_incorrect_aria_attribute_type", `The value of '${attribute}' must be a ${type}`);
w(node, "a11y_incorrect_aria_attribute_type", `The value of '${attribute}' must be a ${type}\nhttps://svelte.dev/e/a11y_incorrect_aria_attribute_type`);
}
/**
@ -241,7 +241,7 @@ export function a11y_incorrect_aria_attribute_type(node, attribute, type) {
* @param {string} attribute
*/
export function a11y_incorrect_aria_attribute_type_boolean(node, attribute) {
w(node, "a11y_incorrect_aria_attribute_type_boolean", `The value of '${attribute}' must be either 'true' or 'false'. It cannot be empty`);
w(node, "a11y_incorrect_aria_attribute_type_boolean", `The value of '${attribute}' must be either 'true' or 'false'. It cannot be empty\nhttps://svelte.dev/e/a11y_incorrect_aria_attribute_type_boolean`);
}
/**
@ -250,7 +250,7 @@ export function a11y_incorrect_aria_attribute_type_boolean(node, attribute) {
* @param {string} attribute
*/
export function a11y_incorrect_aria_attribute_type_id(node, attribute) {
w(node, "a11y_incorrect_aria_attribute_type_id", `The value of '${attribute}' must be a string that represents a DOM element ID`);
w(node, "a11y_incorrect_aria_attribute_type_id", `The value of '${attribute}' must be a string that represents a DOM element ID\nhttps://svelte.dev/e/a11y_incorrect_aria_attribute_type_id`);
}
/**
@ -259,7 +259,7 @@ export function a11y_incorrect_aria_attribute_type_id(node, attribute) {
* @param {string} attribute
*/
export function a11y_incorrect_aria_attribute_type_idlist(node, attribute) {
w(node, "a11y_incorrect_aria_attribute_type_idlist", `The value of '${attribute}' must be a space-separated list of strings that represent DOM element IDs`);
w(node, "a11y_incorrect_aria_attribute_type_idlist", `The value of '${attribute}' must be a space-separated list of strings that represent DOM element IDs\nhttps://svelte.dev/e/a11y_incorrect_aria_attribute_type_idlist`);
}
/**
@ -268,7 +268,7 @@ export function a11y_incorrect_aria_attribute_type_idlist(node, attribute) {
* @param {string} attribute
*/
export function a11y_incorrect_aria_attribute_type_integer(node, attribute) {
w(node, "a11y_incorrect_aria_attribute_type_integer", `The value of '${attribute}' must be an integer`);
w(node, "a11y_incorrect_aria_attribute_type_integer", `The value of '${attribute}' must be an integer\nhttps://svelte.dev/e/a11y_incorrect_aria_attribute_type_integer`);
}
/**
@ -278,7 +278,7 @@ export function a11y_incorrect_aria_attribute_type_integer(node, attribute) {
* @param {string} values
*/
export function a11y_incorrect_aria_attribute_type_token(node, attribute, values) {
w(node, "a11y_incorrect_aria_attribute_type_token", `The value of '${attribute}' must be exactly one of ${values}`);
w(node, "a11y_incorrect_aria_attribute_type_token", `The value of '${attribute}' must be exactly one of ${values}\nhttps://svelte.dev/e/a11y_incorrect_aria_attribute_type_token`);
}
/**
@ -288,7 +288,7 @@ export function a11y_incorrect_aria_attribute_type_token(node, attribute, values
* @param {string} values
*/
export function a11y_incorrect_aria_attribute_type_tokenlist(node, attribute, values) {
w(node, "a11y_incorrect_aria_attribute_type_tokenlist", `The value of '${attribute}' must be a space-separated list of one or more of ${values}`);
w(node, "a11y_incorrect_aria_attribute_type_tokenlist", `The value of '${attribute}' must be a space-separated list of one or more of ${values}\nhttps://svelte.dev/e/a11y_incorrect_aria_attribute_type_tokenlist`);
}
/**
@ -297,7 +297,7 @@ export function a11y_incorrect_aria_attribute_type_tokenlist(node, attribute, va
* @param {string} attribute
*/
export function a11y_incorrect_aria_attribute_type_tristate(node, attribute) {
w(node, "a11y_incorrect_aria_attribute_type_tristate", `The value of '${attribute}' must be exactly one of true, false, or mixed`);
w(node, "a11y_incorrect_aria_attribute_type_tristate", `The value of '${attribute}' must be exactly one of true, false, or mixed\nhttps://svelte.dev/e/a11y_incorrect_aria_attribute_type_tristate`);
}
/**
@ -306,7 +306,7 @@ export function a11y_incorrect_aria_attribute_type_tristate(node, attribute) {
* @param {string} role
*/
export function a11y_interactive_supports_focus(node, role) {
w(node, "a11y_interactive_supports_focus", `Elements with the '${role}' interactive role must have a tabindex value`);
w(node, "a11y_interactive_supports_focus", `Elements with the '${role}' interactive role must have a tabindex value\nhttps://svelte.dev/e/a11y_interactive_supports_focus`);
}
/**
@ -316,7 +316,7 @@ export function a11y_interactive_supports_focus(node, role) {
* @param {string} href_attribute
*/
export function a11y_invalid_attribute(node, href_value, href_attribute) {
w(node, "a11y_invalid_attribute", `'${href_value}' is not a valid ${href_attribute} attribute`);
w(node, "a11y_invalid_attribute", `'${href_value}' is not a valid ${href_attribute} attribute\nhttps://svelte.dev/e/a11y_invalid_attribute`);
}
/**
@ -324,7 +324,7 @@ export function a11y_invalid_attribute(node, href_value, href_attribute) {
* @param {null | NodeLike} node
*/
export function a11y_label_has_associated_control(node) {
w(node, "a11y_label_has_associated_control", "A form label must be associated with a control");
w(node, "a11y_label_has_associated_control", `A form label must be associated with a control\nhttps://svelte.dev/e/a11y_label_has_associated_control`);
}
/**
@ -332,7 +332,7 @@ export function a11y_label_has_associated_control(node) {
* @param {null | NodeLike} node
*/
export function a11y_media_has_caption(node) {
w(node, "a11y_media_has_caption", "`<video>` elements must have a `<track kind=\"captions\">`");
w(node, "a11y_media_has_caption", `\`<video>\` elements must have a \`<track kind="captions">\`\nhttps://svelte.dev/e/a11y_media_has_caption`);
}
/**
@ -341,7 +341,7 @@ export function a11y_media_has_caption(node) {
* @param {string} name
*/
export function a11y_misplaced_role(node, name) {
w(node, "a11y_misplaced_role", `\`<${name}>\` should not have role attribute`);
w(node, "a11y_misplaced_role", `\`<${name}>\` should not have role attribute\nhttps://svelte.dev/e/a11y_misplaced_role`);
}
/**
@ -349,7 +349,7 @@ export function a11y_misplaced_role(node, name) {
* @param {null | NodeLike} node
*/
export function a11y_misplaced_scope(node) {
w(node, "a11y_misplaced_scope", "The scope attribute should only be used with `<th>` elements");
w(node, "a11y_misplaced_scope", `The scope attribute should only be used with \`<th>\` elements\nhttps://svelte.dev/e/a11y_misplaced_scope`);
}
/**
@ -360,7 +360,7 @@ export function a11y_misplaced_scope(node) {
* @param {string} sequence
*/
export function a11y_missing_attribute(node, name, article, sequence) {
w(node, "a11y_missing_attribute", `\`<${name}>\` element should have ${article} ${sequence} attribute`);
w(node, "a11y_missing_attribute", `\`<${name}>\` element should have ${article} ${sequence} attribute\nhttps://svelte.dev/e/a11y_missing_attribute`);
}
/**
@ -369,7 +369,7 @@ export function a11y_missing_attribute(node, name, article, sequence) {
* @param {string} name
*/
export function a11y_missing_content(node, name) {
w(node, "a11y_missing_content", `\`<${name}>\` element should contain text`);
w(node, "a11y_missing_content", `\`<${name}>\` element should contain text\nhttps://svelte.dev/e/a11y_missing_content`);
}
/**
@ -379,7 +379,7 @@ export function a11y_missing_content(node, name) {
* @param {string} accompanied_by
*/
export function a11y_mouse_events_have_key_events(node, event, accompanied_by) {
w(node, "a11y_mouse_events_have_key_events", `'${event}' event must be accompanied by '${accompanied_by}' event`);
w(node, "a11y_mouse_events_have_key_events", `'${event}' event must be accompanied by '${accompanied_by}' event\nhttps://svelte.dev/e/a11y_mouse_events_have_key_events`);
}
/**
@ -388,7 +388,7 @@ export function a11y_mouse_events_have_key_events(node, event, accompanied_by) {
* @param {string} role
*/
export function a11y_no_abstract_role(node, role) {
w(node, "a11y_no_abstract_role", `Abstract role '${role}' is forbidden`);
w(node, "a11y_no_abstract_role", `Abstract role '${role}' is forbidden\nhttps://svelte.dev/e/a11y_no_abstract_role`);
}
/**
@ -398,7 +398,7 @@ export function a11y_no_abstract_role(node, role) {
* @param {string} role
*/
export function a11y_no_interactive_element_to_noninteractive_role(node, element, role) {
w(node, "a11y_no_interactive_element_to_noninteractive_role", `\`<${element}>\` cannot have role '${role}'`);
w(node, "a11y_no_interactive_element_to_noninteractive_role", `\`<${element}>\` cannot have role '${role}'\nhttps://svelte.dev/e/a11y_no_interactive_element_to_noninteractive_role`);
}
/**
@ -407,7 +407,7 @@ export function a11y_no_interactive_element_to_noninteractive_role(node, element
* @param {string} element
*/
export function a11y_no_noninteractive_element_interactions(node, element) {
w(node, "a11y_no_noninteractive_element_interactions", `Non-interactive element \`<${element}>\` should not be assigned mouse or keyboard event listeners`);
w(node, "a11y_no_noninteractive_element_interactions", `Non-interactive element \`<${element}>\` should not be assigned mouse or keyboard event listeners\nhttps://svelte.dev/e/a11y_no_noninteractive_element_interactions`);
}
/**
@ -417,7 +417,7 @@ export function a11y_no_noninteractive_element_interactions(node, element) {
* @param {string} role
*/
export function a11y_no_noninteractive_element_to_interactive_role(node, element, role) {
w(node, "a11y_no_noninteractive_element_to_interactive_role", `Non-interactive element \`<${element}>\` cannot have interactive role '${role}'`);
w(node, "a11y_no_noninteractive_element_to_interactive_role", `Non-interactive element \`<${element}>\` cannot have interactive role '${role}'\nhttps://svelte.dev/e/a11y_no_noninteractive_element_to_interactive_role`);
}
/**
@ -425,7 +425,7 @@ export function a11y_no_noninteractive_element_to_interactive_role(node, element
* @param {null | NodeLike} node
*/
export function a11y_no_noninteractive_tabindex(node) {
w(node, "a11y_no_noninteractive_tabindex", "noninteractive element cannot have nonnegative tabIndex value");
w(node, "a11y_no_noninteractive_tabindex", `noninteractive element cannot have nonnegative tabIndex value\nhttps://svelte.dev/e/a11y_no_noninteractive_tabindex`);
}
/**
@ -434,7 +434,7 @@ export function a11y_no_noninteractive_tabindex(node) {
* @param {string} role
*/
export function a11y_no_redundant_roles(node, role) {
w(node, "a11y_no_redundant_roles", `Redundant role '${role}'`);
w(node, "a11y_no_redundant_roles", `Redundant role '${role}'\nhttps://svelte.dev/e/a11y_no_redundant_roles`);
}
/**
@ -444,7 +444,7 @@ export function a11y_no_redundant_roles(node, role) {
* @param {string} handler
*/
export function a11y_no_static_element_interactions(node, element, handler) {
w(node, "a11y_no_static_element_interactions", `\`<${element}>\` with a ${handler} handler must have an ARIA role`);
w(node, "a11y_no_static_element_interactions", `\`<${element}>\` with a ${handler} handler must have an ARIA role\nhttps://svelte.dev/e/a11y_no_static_element_interactions`);
}
/**
@ -452,7 +452,7 @@ export function a11y_no_static_element_interactions(node, element, handler) {
* @param {null | NodeLike} node
*/
export function a11y_positive_tabindex(node) {
w(node, "a11y_positive_tabindex", "Avoid tabindex values above zero");
w(node, "a11y_positive_tabindex", `Avoid tabindex values above zero\nhttps://svelte.dev/e/a11y_positive_tabindex`);
}
/**
@ -462,7 +462,7 @@ export function a11y_positive_tabindex(node) {
* @param {string} props
*/
export function a11y_role_has_required_aria_props(node, role, props) {
w(node, "a11y_role_has_required_aria_props", `Elements with the ARIA role "${role}" must have the following attributes defined: ${props}`);
w(node, "a11y_role_has_required_aria_props", `Elements with the ARIA role "${role}" must have the following attributes defined: ${props}\nhttps://svelte.dev/e/a11y_role_has_required_aria_props`);
}
/**
@ -472,7 +472,7 @@ export function a11y_role_has_required_aria_props(node, role, props) {
* @param {string} role
*/
export function a11y_role_supports_aria_props(node, attribute, role) {
w(node, "a11y_role_supports_aria_props", `The attribute '${attribute}' is not supported by the role '${role}'`);
w(node, "a11y_role_supports_aria_props", `The attribute '${attribute}' is not supported by the role '${role}'\nhttps://svelte.dev/e/a11y_role_supports_aria_props`);
}
/**
@ -483,7 +483,7 @@ export function a11y_role_supports_aria_props(node, attribute, role) {
* @param {string} name
*/
export function a11y_role_supports_aria_props_implicit(node, attribute, role, name) {
w(node, "a11y_role_supports_aria_props_implicit", `The attribute '${attribute}' is not supported by the role '${role}'. This role is implicit on the element \`<${name}>\``);
w(node, "a11y_role_supports_aria_props_implicit", `The attribute '${attribute}' is not supported by the role '${role}'. This role is implicit on the element \`<${name}>\`\nhttps://svelte.dev/e/a11y_role_supports_aria_props_implicit`);
}
/**
@ -493,7 +493,7 @@ export function a11y_role_supports_aria_props_implicit(node, attribute, role, na
* @param {string | undefined | null} [suggestion]
*/
export function a11y_unknown_aria_attribute(node, attribute, suggestion) {
w(node, "a11y_unknown_aria_attribute", suggestion ? `Unknown aria attribute 'aria-${attribute}'. Did you mean '${suggestion}'?` : `Unknown aria attribute 'aria-${attribute}'`);
w(node, "a11y_unknown_aria_attribute", `${suggestion ? `Unknown aria attribute 'aria-${attribute}'. Did you mean '${suggestion}'?` : `Unknown aria attribute 'aria-${attribute}'`}\nhttps://svelte.dev/e/a11y_unknown_aria_attribute`);
}
/**
@ -503,7 +503,7 @@ export function a11y_unknown_aria_attribute(node, attribute, suggestion) {
* @param {string | undefined | null} [suggestion]
*/
export function a11y_unknown_role(node, role, suggestion) {
w(node, "a11y_unknown_role", suggestion ? `Unknown role '${role}'. Did you mean '${suggestion}'?` : `Unknown role '${role}'`);
w(node, "a11y_unknown_role", `${suggestion ? `Unknown role '${role}'. Did you mean '${suggestion}'?` : `Unknown role '${role}'`}\nhttps://svelte.dev/e/a11y_unknown_role`);
}
/**
@ -513,7 +513,7 @@ export function a11y_unknown_role(node, role, suggestion) {
* @param {string} suggestion
*/
export function legacy_code(node, code, suggestion) {
w(node, "legacy_code", `\`${code}\` is no longer valid — please use \`${suggestion}\` instead`);
w(node, "legacy_code", `\`${code}\` is no longer valid — please use \`${suggestion}\` instead\nhttps://svelte.dev/e/legacy_code`);
}
/**
@ -523,7 +523,7 @@ export function legacy_code(node, code, suggestion) {
* @param {string | undefined | null} [suggestion]
*/
export function unknown_code(node, code, suggestion) {
w(node, "unknown_code", suggestion ? `\`${code}\` is not a recognised code (did you mean \`${suggestion}\`?)` : `\`${code}\` is not a recognised code`);
w(node, "unknown_code", `${suggestion ? `\`${code}\` is not a recognised code (did you mean \`${suggestion}\`?)` : `\`${code}\` is not a recognised code`}\nhttps://svelte.dev/e/unknown_code`);
}
/**
@ -531,7 +531,7 @@ export function unknown_code(node, code, suggestion) {
* @param {null | NodeLike} node
*/
export function options_deprecated_accessors(node) {
w(node, "options_deprecated_accessors", "The `accessors` option has been deprecated. It will have no effect in runes mode");
w(node, "options_deprecated_accessors", `The \`accessors\` option has been deprecated. It will have no effect in runes mode\nhttps://svelte.dev/e/options_deprecated_accessors`);
}
/**
@ -539,7 +539,7 @@ export function options_deprecated_accessors(node) {
* @param {null | NodeLike} node
*/
export function options_deprecated_immutable(node) {
w(node, "options_deprecated_immutable", "The `immutable` option has been deprecated. It will have no effect in runes mode");
w(node, "options_deprecated_immutable", `The \`immutable\` option has been deprecated. It will have no effect in runes mode\nhttps://svelte.dev/e/options_deprecated_immutable`);
}
/**
@ -547,7 +547,7 @@ export function options_deprecated_immutable(node) {
* @param {null | NodeLike} node
*/
export function options_missing_custom_element(node) {
w(node, "options_missing_custom_element", "The `customElement` option is used when generating a custom element. Did you forget the `customElement: true` compile option?");
w(node, "options_missing_custom_element", `The \`customElement\` option is used when generating a custom element. Did you forget the \`customElement: true\` compile option?\nhttps://svelte.dev/e/options_missing_custom_element`);
}
/**
@ -555,7 +555,7 @@ export function options_missing_custom_element(node) {
* @param {null | NodeLike} node
*/
export function options_removed_enable_sourcemap(node) {
w(node, "options_removed_enable_sourcemap", "The `enableSourcemap` option has been removed. Source maps are always generated now, and tooling can choose to ignore them");
w(node, "options_removed_enable_sourcemap", `The \`enableSourcemap\` option has been removed. Source maps are always generated now, and tooling can choose to ignore them\nhttps://svelte.dev/e/options_removed_enable_sourcemap`);
}
/**
@ -563,7 +563,7 @@ export function options_removed_enable_sourcemap(node) {
* @param {null | NodeLike} node
*/
export function options_removed_hydratable(node) {
w(node, "options_removed_hydratable", "The `hydratable` option has been removed. Svelte components are always hydratable now");
w(node, "options_removed_hydratable", `The \`hydratable\` option has been removed. Svelte components are always hydratable now\nhttps://svelte.dev/e/options_removed_hydratable`);
}
/**
@ -571,7 +571,7 @@ export function options_removed_hydratable(node) {
* @param {null | NodeLike} node
*/
export function options_removed_loop_guard_timeout(node) {
w(node, "options_removed_loop_guard_timeout", "The `loopGuardTimeout` option has been removed");
w(node, "options_removed_loop_guard_timeout", `The \`loopGuardTimeout\` option has been removed\nhttps://svelte.dev/e/options_removed_loop_guard_timeout`);
}
/**
@ -579,7 +579,7 @@ export function options_removed_loop_guard_timeout(node) {
* @param {null | NodeLike} node
*/
export function options_renamed_ssr_dom(node) {
w(node, "options_renamed_ssr_dom", "`generate: \"dom\"` and `generate: \"ssr\"` options have been renamed to \"client\" and \"server\" respectively");
w(node, "options_renamed_ssr_dom", `\`generate: "dom"\` and \`generate: "ssr"\` options have been renamed to "client" and "server" respectively\nhttps://svelte.dev/e/options_renamed_ssr_dom`);
}
/**
@ -588,7 +588,7 @@ export function options_renamed_ssr_dom(node) {
* @param {string} name
*/
export function export_let_unused(node, name) {
w(node, "export_let_unused", `Component has unused export property '${name}'. If it is for external reference only, please consider using \`export const ${name}\``);
w(node, "export_let_unused", `Component has unused export property '${name}'. If it is for external reference only, please consider using \`export const ${name}\`\nhttps://svelte.dev/e/export_let_unused`);
}
/**
@ -596,7 +596,7 @@ export function export_let_unused(node, name) {
* @param {null | NodeLike} node
*/
export function legacy_component_creation(node) {
w(node, "legacy_component_creation", "Svelte 5 components are no longer classes. Instantiate them using `mount` or `hydrate` (imported from 'svelte') instead.");
w(node, "legacy_component_creation", `Svelte 5 components are no longer classes. Instantiate them using \`mount\` or \`hydrate\` (imported from 'svelte') instead.\nhttps://svelte.dev/e/legacy_component_creation`);
}
/**
@ -605,7 +605,7 @@ export function legacy_component_creation(node) {
* @param {string} name
*/
export function non_reactive_update(node, name) {
w(node, "non_reactive_update", `\`${name}\` is updated, but is not declared with \`$state(...)\`. Changing its value will not correctly trigger updates`);
w(node, "non_reactive_update", `\`${name}\` is updated, but is not declared with \`$state(...)\`. Changing its value will not correctly trigger updates\nhttps://svelte.dev/e/non_reactive_update`);
}
/**
@ -613,7 +613,7 @@ export function non_reactive_update(node, name) {
* @param {null | NodeLike} node
*/
export function perf_avoid_inline_class(node) {
w(node, "perf_avoid_inline_class", "Avoid 'new class' — instead, declare the class at the top level scope");
w(node, "perf_avoid_inline_class", `Avoid 'new class' — instead, declare the class at the top level scope\nhttps://svelte.dev/e/perf_avoid_inline_class`);
}
/**
@ -621,7 +621,7 @@ export function perf_avoid_inline_class(node) {
* @param {null | NodeLike} node
*/
export function perf_avoid_nested_class(node) {
w(node, "perf_avoid_nested_class", "Avoid declaring classes below the top level scope");
w(node, "perf_avoid_nested_class", `Avoid declaring classes below the top level scope\nhttps://svelte.dev/e/perf_avoid_nested_class`);
}
/**
@ -629,7 +629,7 @@ export function perf_avoid_nested_class(node) {
* @param {null | NodeLike} node
*/
export function reactive_declaration_invalid_placement(node) {
w(node, "reactive_declaration_invalid_placement", "Reactive declarations only exist at the top level of the instance script");
w(node, "reactive_declaration_invalid_placement", `Reactive declarations only exist at the top level of the instance script\nhttps://svelte.dev/e/reactive_declaration_invalid_placement`);
}
/**
@ -637,7 +637,7 @@ export function reactive_declaration_invalid_placement(node) {
* @param {null | NodeLike} node
*/
export function reactive_declaration_module_script_dependency(node) {
w(node, "reactive_declaration_module_script_dependency", "Reassignments of module-level declarations will not cause reactive statements to update");
w(node, "reactive_declaration_module_script_dependency", `Reassignments of module-level declarations will not cause reactive statements to update\nhttps://svelte.dev/e/reactive_declaration_module_script_dependency`);
}
/**
@ -645,7 +645,7 @@ export function reactive_declaration_module_script_dependency(node) {
* @param {null | NodeLike} node
*/
export function state_referenced_locally(node) {
w(node, "state_referenced_locally", "State referenced in its own scope will never update. Did you mean to reference it inside a closure?");
w(node, "state_referenced_locally", `State referenced in its own scope will never update. Did you mean to reference it inside a closure?\nhttps://svelte.dev/e/state_referenced_locally`);
}
/**
@ -654,7 +654,7 @@ export function state_referenced_locally(node) {
* @param {string} name
*/
export function store_rune_conflict(node, name) {
w(node, "store_rune_conflict", `It looks like you're using the \`$${name}\` rune, but there is a local binding called \`${name}\`. Referencing a local variable with a \`$\` prefix will create a store subscription. Please rename \`${name}\` to avoid the ambiguity`);
w(node, "store_rune_conflict", `It looks like you're using the \`$${name}\` rune, but there is a local binding called \`${name}\`. Referencing a local variable with a \`$\` prefix will create a store subscription. Please rename \`${name}\` to avoid the ambiguity\nhttps://svelte.dev/e/store_rune_conflict`);
}
/**
@ -663,7 +663,7 @@ export function store_rune_conflict(node, name) {
* @param {string} name
*/
export function css_unused_selector(node, name) {
w(node, "css_unused_selector", `Unused CSS selector "${name}"`);
w(node, "css_unused_selector", `Unused CSS selector "${name}"\nhttps://svelte.dev/e/css_unused_selector`);
}
/**
@ -671,7 +671,7 @@ export function css_unused_selector(node, name) {
* @param {null | NodeLike} node
*/
export function attribute_avoid_is(node) {
w(node, "attribute_avoid_is", "The \"is\" attribute is not supported cross-browser and should be avoided");
w(node, "attribute_avoid_is", `The "is" attribute is not supported cross-browser and should be avoided\nhttps://svelte.dev/e/attribute_avoid_is`);
}
/**
@ -680,7 +680,7 @@ export function attribute_avoid_is(node) {
* @param {string} name
*/
export function attribute_global_event_reference(node, name) {
w(node, "attribute_global_event_reference", `You are referencing \`globalThis.${name}\`. Did you forget to declare a variable with that name?`);
w(node, "attribute_global_event_reference", `You are referencing \`globalThis.${name}\`. Did you forget to declare a variable with that name?\nhttps://svelte.dev/e/attribute_global_event_reference`);
}
/**
@ -688,7 +688,7 @@ export function attribute_global_event_reference(node, name) {
* @param {null | NodeLike} node
*/
export function attribute_illegal_colon(node) {
w(node, "attribute_illegal_colon", "Attributes should not contain ':' characters to prevent ambiguity with Svelte directives");
w(node, "attribute_illegal_colon", `Attributes should not contain ':' characters to prevent ambiguity with Svelte directives\nhttps://svelte.dev/e/attribute_illegal_colon`);
}
/**
@ -698,7 +698,7 @@ export function attribute_illegal_colon(node) {
* @param {string} right
*/
export function attribute_invalid_property_name(node, wrong, right) {
w(node, "attribute_invalid_property_name", `'${wrong}' is not a valid HTML attribute. Did you mean '${right}'?`);
w(node, "attribute_invalid_property_name", `'${wrong}' is not a valid HTML attribute. Did you mean '${right}'?\nhttps://svelte.dev/e/attribute_invalid_property_name`);
}
/**
@ -706,7 +706,7 @@ export function attribute_invalid_property_name(node, wrong, right) {
* @param {null | NodeLike} node
*/
export function attribute_quoted(node) {
w(node, "attribute_quoted", "Quoted attributes on components and custom elements will be stringified in a future version of Svelte. If this isn't what you want, remove the quotes");
w(node, "attribute_quoted", `Quoted attributes on components and custom elements will be stringified in a future version of Svelte. If this isn't what you want, remove the quotes\nhttps://svelte.dev/e/attribute_quoted`);
}
/**
@ -715,7 +715,7 @@ export function attribute_quoted(node) {
* @param {string} name
*/
export function bind_invalid_each_rest(node, name) {
w(node, "bind_invalid_each_rest", `The rest operator (...) will create a new object and binding '${name}' with the original object will not work`);
w(node, "bind_invalid_each_rest", `The rest operator (...) will create a new object and binding '${name}' with the original object will not work\nhttps://svelte.dev/e/bind_invalid_each_rest`);
}
/**
@ -723,7 +723,7 @@ export function bind_invalid_each_rest(node, name) {
* @param {null | NodeLike} node
*/
export function block_empty(node) {
w(node, "block_empty", "Empty block");
w(node, "block_empty", `Empty block\nhttps://svelte.dev/e/block_empty`);
}
/**
@ -732,7 +732,7 @@ export function block_empty(node) {
* @param {string} name
*/
export function component_name_lowercase(node, name) {
w(node, "component_name_lowercase", `\`<${name}>\` will be treated as an HTML element unless it begins with a capital letter`);
w(node, "component_name_lowercase", `\`<${name}>\` will be treated as an HTML element unless it begins with a capital letter\nhttps://svelte.dev/e/component_name_lowercase`);
}
/**
@ -741,7 +741,7 @@ export function component_name_lowercase(node, name) {
* @param {string} name
*/
export function element_invalid_self_closing_tag(node, name) {
w(node, "element_invalid_self_closing_tag", `Self-closing HTML tags for non-void elements are ambiguous — use \`<${name} ...></${name}>\` rather than \`<${name} ... />\``);
w(node, "element_invalid_self_closing_tag", `Self-closing HTML tags for non-void elements are ambiguous — use \`<${name} ...></${name}>\` rather than \`<${name} ... />\`\nhttps://svelte.dev/e/element_invalid_self_closing_tag`);
}
/**
@ -750,7 +750,7 @@ export function element_invalid_self_closing_tag(node, name) {
* @param {string} name
*/
export function event_directive_deprecated(node, name) {
w(node, "event_directive_deprecated", `Using \`on:${name}\` to listen to the ${name} event is deprecated. Use the event attribute \`on${name}\` instead`);
w(node, "event_directive_deprecated", `Using \`on:${name}\` to listen to the ${name} event is deprecated. Use the event attribute \`on${name}\` instead\nhttps://svelte.dev/e/event_directive_deprecated`);
}
/**
@ -759,7 +759,7 @@ export function event_directive_deprecated(node, name) {
* @param {string} message
*/
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`);
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\nhttps://svelte.dev/e/node_invalid_placement_ssr`);
}
/**
@ -767,7 +767,7 @@ export function node_invalid_placement_ssr(node, message) {
* @param {null | NodeLike} node
*/
export function script_context_deprecated(node) {
w(node, "script_context_deprecated", "`context=\"module\"` is deprecated, use the `module` attribute instead");
w(node, "script_context_deprecated", `\`context="module"\` is deprecated, use the \`module\` attribute instead\nhttps://svelte.dev/e/script_context_deprecated`);
}
/**
@ -775,7 +775,7 @@ export function script_context_deprecated(node) {
* @param {null | NodeLike} node
*/
export function script_unknown_attribute(node) {
w(node, "script_unknown_attribute", "Unrecognized attribute — should be one of `generics`, `lang` or `module`. If this exists for a preprocessor, ensure that the preprocessor removes it");
w(node, "script_unknown_attribute", `Unrecognized attribute — should be one of \`generics\`, \`lang\` or \`module\`. If this exists for a preprocessor, ensure that the preprocessor removes it\nhttps://svelte.dev/e/script_unknown_attribute`);
}
/**
@ -783,7 +783,7 @@ export function script_unknown_attribute(node) {
* @param {null | NodeLike} node
*/
export function slot_element_deprecated(node) {
w(node, "slot_element_deprecated", "Using `<slot>` to render parent content is deprecated. Use `{@render ...}` tags instead");
w(node, "slot_element_deprecated", `Using \`<slot>\` to render parent content is deprecated. Use \`{@render ...}\` tags instead\nhttps://svelte.dev/e/slot_element_deprecated`);
}
/**
@ -791,7 +791,7 @@ export function slot_element_deprecated(node) {
* @param {null | NodeLike} node
*/
export function svelte_component_deprecated(node) {
w(node, "svelte_component_deprecated", "`<svelte:component>` is deprecated in runes mode — components are dynamic by default");
w(node, "svelte_component_deprecated", `\`<svelte:component>\` is deprecated in runes mode — components are dynamic by default\nhttps://svelte.dev/e/svelte_component_deprecated`);
}
/**
@ -799,7 +799,7 @@ export function svelte_component_deprecated(node) {
* @param {null | NodeLike} node
*/
export function svelte_element_invalid_this(node) {
w(node, "svelte_element_invalid_this", "`this` should be an `{expression}`. Using a string attribute value will cause an error in future versions of Svelte");
w(node, "svelte_element_invalid_this", `\`this\` should be an \`{expression}\`. Using a string attribute value will cause an error in future versions of Svelte\nhttps://svelte.dev/e/svelte_element_invalid_this`);
}
/**
@ -809,5 +809,5 @@ export function svelte_element_invalid_this(node) {
* @param {string} basename
*/
export function svelte_self_deprecated(node, name, basename) {
w(node, "svelte_self_deprecated", `\`<svelte:self>\` is deprecated — use self-imports (e.g. \`import ${name} from './${basename}'\`) instead`);
w(node, "svelte_self_deprecated", `\`<svelte:self>\` is deprecated — use self-imports (e.g. \`import ${name} from './${basename}'\`) instead\nhttps://svelte.dev/e/svelte_self_deprecated`);
}

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

@ -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}
*/

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

@ -1,3 +1,4 @@
import { untrack } from '../runtime.js';
import * as w from '../warnings.js';
import { sanitize_location } from './location.js';
@ -23,7 +24,12 @@ function compare(a, b, property, location) {
* @param {string} location
*/
export function assign(object, property, value, location) {
return compare((object[property] = value), object[property], property, location);
return compare(
(object[property] = value),
untrack(() => object[property]),
property,
location
);
}
/**
@ -33,7 +39,12 @@ export function assign(object, property, value, location) {
* @param {string} location
*/
export function assign_and(object, property, value, location) {
return compare((object[property] &&= value), object[property], property, location);
return compare(
(object[property] &&= value),
untrack(() => object[property]),
property,
location
);
}
/**
@ -43,7 +54,12 @@ export function assign_and(object, property, value, location) {
* @param {string} location
*/
export function assign_or(object, property, value, location) {
return compare((object[property] ||= value), object[property], property, location);
return compare(
(object[property] ||= value),
untrack(() => object[property]),
property,
location
);
}
/**
@ -53,5 +69,10 @@ export function assign_or(object, property, value, location) {
* @param {string} location
*/
export function assign_nullish(object, property, value, location) {
return compare((object[property] ??= value), object[property], property, location);
return compare(
(object[property] ??= value),
untrack(() => object[property]),
property,
location
);
}

@ -9,7 +9,7 @@ import {
set_hydrating
} from '../hydration.js';
import { block, branch, pause_effect, resume_effect } from '../../reactivity/effects.js';
import { HYDRATION_START_ELSE } from '../../../../constants.js';
import { HYDRATION_START_ELSE, UNINITIALIZED } from '../../../../constants.js';
/**
* @param {TemplateNode} node
@ -30,8 +30,8 @@ export function if_block(node, fn, elseif = false) {
/** @type {Effect | null} */
var alternate_effect = null;
/** @type {boolean | null} */
var condition = null;
/** @type {UNINITIALIZED | boolean | null} */
var condition = UNINITIALIZED;
var flags = elseif ? EFFECT_TRANSPARENT : 0;
@ -54,7 +54,7 @@ export function if_block(node, fn, elseif = false) {
if (hydrating) {
const is_else = /** @type {Comment} */ (anchor).data === HYDRATION_START_ELSE;
if (condition === is_else) {
if (!!condition === is_else) {
// Hydration mismatch: remove everything inside the anchor and start fresh.
// This could happen with `{#if browser}...{/if}`, for example
anchor = remove_nodes();

@ -21,6 +21,7 @@ import { component_context, active_effect } from '../../runtime.js';
import { DEV } from 'esm-env';
import { EFFECT_TRANSPARENT } from '../../constants.js';
import { assign_nodes } from '../template.js';
import { is_raw_text_element } from '../../../../utils.js';
/**
* @param {Comment | Element} node
@ -116,6 +117,11 @@ export function element(node, get_tag, is_svg, render_fn, get_namespace, locatio
assign_nodes(element, element);
if (render_fn) {
if (hydrating && is_raw_text_element(next_tag)) {
// prevent hydration glitches
element.append(document.createComment(''));
}
// If hydrating, use the existing ssr comment as the anchor so that the
// inner open and close methods can pick up the existing nodes correctly
var child_anchor = /** @type {TemplateNode} */ (

@ -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,7 +85,15 @@ 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;
}
@ -187,7 +201,7 @@ export function set_xlink_attribute(dom, attribute, value) {
}
/**
* @param {any} node
* @param {HTMLElement} node
* @param {string} prop
* @param {any} value
*/
@ -202,10 +216,21 @@ export function set_custom_element_data(node, prop, value) {
set_active_reaction(null);
set_active_effect(null);
try {
if (get_setters(node).includes(prop)) {
if (
// Don't compute setters for custom elements while they aren't registered yet,
// because during their upgrade/instantiation they might add more setters.
// Instead, fall back to a simple "an object, then set as property" heuristic.
setters_cache.has(node.nodeName) || customElements.get(node.tagName.toLowerCase())
? get_setters(node).includes(prop)
: value && typeof value === 'object'
) {
// @ts-expect-error
node[prop] = value;
} else {
set_attribute(node, prop, value);
// We did getters etc checks already, stringify before passing to set_attribute
// to ensure it doesn't invoke the same logic again, and potentially populating
// the setters cache too early.
set_attribute(node, prop, value == null ? value : String(value));
}
} finally {
set_active_reaction(previous_reaction);
@ -333,16 +358,39 @@ export function set_attributes(
} else if (key === '__value' || (key === 'value' && value != null)) {
// @ts-ignore
element.value = element[key] = element.__value = value;
} else if (key === 'selected' && is_option_element) {
set_selected(/** @type {HTMLOptionElement} */ (element), value);
} else {
var name = key;
if (!preserve_attribute_case) {
name = normalize_attribute(name);
}
if (value == null && !is_custom_element) {
var is_default = name === 'defaultValue' || name === 'defaultChecked';
if (value == null && !is_custom_element && !is_default) {
attributes[key] = null;
element.removeAttribute(key);
} else if (setters.includes(name) && (is_custom_element || typeof value !== 'string')) {
if (name === 'value' || name === 'checked') {
// removing value/checked also removes defaultValue/defaultChecked — preserve
let input = /** @type {HTMLInputElement} */ (element);
if (name === 'value') {
let prev = input.defaultValue;
input.removeAttribute(name);
input.defaultValue = prev;
} else {
let prev = input.defaultChecked;
input.removeAttribute(name);
input.defaultChecked = prev;
}
} else {
element.removeAttribute(key);
}
} else if (
is_default ||
(setters.includes(name) && (is_custom_element || typeof value !== 'string'))
) {
// @ts-ignore
element[name] = value;
} else if (typeof value !== 'function') {
@ -370,8 +418,9 @@ function get_setters(element) {
var setters = setters_cache.get(element.nodeName);
if (setters) return setters;
setters_cache.set(element.nodeName, (setters = []));
var descriptors;
var proto = get_prototype_of(element);
var proto = element; // In the case of custom elements there might be setters on the instance
var element_proto = Element.prototype;
// Stop at Element, from there on there's only unnecessary setters we're not interested in

@ -30,8 +30,17 @@ export function bind_value(input, get, set = get) {
// 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())) {
var start = input.selectionStart;
var end = input.selectionEnd;
// the value is coerced on assignment
input.value = value ?? '';
// Restore selection
if (end !== null) {
input.selectionStart = start;
input.selectionEnd = Math.min(end, input.value.length);
}
}
});

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

Loading…
Cancel
Save