Merge remote-tracking branch 'origin/main' into thunkify-deriveds-on-server

thunkify-deriveds-on-server
paoloricciuti 3 months ago
commit 22e666c438

@ -1,5 +0,0 @@
---
'svelte': patch
---
fix: improve error message for migration errors when slot would be renamed

@ -1,5 +0,0 @@
---
'svelte': patch
---
fix: allow characters in the supplementary special-purpose plane

@ -28,6 +28,8 @@ jobs:
os: ubuntu-latest
- node-version: 22
os: ubuntu-latest
- node-version: 24
os: ubuntu-latest
steps:
- uses: actions/checkout@v4

@ -67,19 +67,29 @@ for (let i = 0; i < results[0].length; i += 1) {
for (const metric of ['time', 'gc_time']) {
const times = results.map((result) => +result[i][metric]);
let min = Infinity;
let max = -Infinity;
let min_index = -1;
for (let b = 0; b < times.length; b += 1) {
if (times[b] < min) {
min = times[b];
const time = times[b];
if (time < min) {
min = time;
min_index = b;
}
if (time > max) {
max = time;
}
}
if (min !== 0) {
console.group(`${metric}: fastest is ${branches[min_index]}`);
console.group(`${metric}: fastest is ${char(min_index)} (${branches[min_index]})`);
times.forEach((time, b) => {
console.log(`${branches[b]}: ${time.toFixed(2)}ms (${((time / min) * 100).toFixed(2)}%)`);
const SIZE = 20;
const n = Math.round(SIZE * (time / max));
console.log(`${char(b)}: ${'◼'.repeat(n)}${' '.repeat(SIZE - n)} ${time.toFixed(2)}ms`);
});
console.groupEnd();
}
@ -87,3 +97,7 @@ for (let i = 0; i < results[0].length; i += 1) {
console.groupEnd();
}
function char(i) {
return String.fromCharCode(97 + i);
}

@ -20,9 +20,7 @@ Unlike other frameworks you may have encountered, there is no API for interactin
If `$state` is used with an array or a simple object, the result is a deeply reactive _state proxy_. [Proxies](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy) allow Svelte to run code when you read or write properties, including via methods like `array.push(...)`, triggering granular updates.
> [!NOTE] Classes like `Set` and `Map` will not be proxied, but Svelte provides reactive implementations for various built-ins like these that can be imported from [`svelte/reactivity`](./svelte-reactivity).
State is proxified recursively until Svelte finds something other than an array or simple object. In a case like this...
State is proxified recursively until Svelte finds something other than an array or simple object (like a class or an object created with `Object.create`). In a case like this...
```js
let todos = $state([
@ -67,16 +65,15 @@ todos[0].done = !todos[0].done;
### Classes
You can also use `$state` in class fields (whether public or private):
Class instances are not proxied. Instead, you can use `$state` in class fields (whether public or private), or as the first assignment to a property immediately inside the `constructor`:
```js
// @errors: 7006 2554
class Todo {
done = $state(false);
text = $state();
constructor(text) {
this.text = text;
this.text = $state(text);
}
reset() {
@ -110,10 +107,9 @@ You can either use an inline function...
// @errors: 7006 2554
class Todo {
done = $state(false);
text = $state();
constructor(text) {
this.text = text;
this.text = $state(text);
}
+++reset = () => {+++
@ -123,6 +119,8 @@ class Todo {
}
```
> Svelte provides reactive implementations of built-in classes like `Set` and `Map` that can be imported from [`svelte/reactivity`](svelte-reactivity).
## `$state.raw`
In cases where you don't want objects and arrays to be deeply reactive you can use `$state.raw`.
@ -147,6 +145,8 @@ person = {
This can improve performance with large arrays and objects that you weren't planning to mutate anyway, since it avoids the cost of making them reactive. Note that raw state can _contain_ reactive state (for example, a raw array of reactive objects).
As with `$state`, you can declare class fields using `$state.raw`.
## `$state.snapshot`
To take a static snapshot of a deeply reactive `$state` proxy, use `$state.snapshot`:

@ -269,11 +269,11 @@ In general, `$effect` is best considered something of an escape hatch — useful
If you're using an effect because you want to be able to reassign the derived value (to build an optimistic UI, for example) note that [deriveds can be directly overridden]($derived#Overriding-derived-values) as of Svelte 5.25.
You might be tempted to do something convoluted with effects to link one value to another. The following example shows two inputs for "money spent" and "money left" that are connected to each other. If you update one, the other should update accordingly. Don't use effects for this ([demo](/playground/untitled#H4sIAAAAAAAACpVRy26DMBD8FcvKgUhtoIdeHBwp31F6MGSJkBbHwksEQvx77aWQqooq9bgzOzP7mGTdIHipPiZJowOpGJAv0po2VmfnDv4OSBErjYdneHWzBJaCjcx91TWOToUtCIEE3cig0OIty44r5l1oDtjOkyFIsv3GINQ_CNYyGegd1DVUlCR7oU9iilDUcP8S8roYs9n8p2wdYNVFm4csTx872BxNCcjr5I11fdgonEkXsjP2CoUUZWMv6m6wBz2x7yxaM-iJvWeRsvSbSVeUy5i0uf8vKA78NIeJLSZWv1I8jQjLdyK4XuTSeIdmVKJGGI4LdjVOiezwDu1yG74My8PLCQaSiroe5s_5C2PHrkVGAgAA)):
You might be tempted to do something convoluted with effects to link one value to another. The following example shows two inputs for "money spent" and "money left" that are connected to each other. If you update one, the other should update accordingly. Don't use effects for this ([demo](/playground/untitled#H4sIAAAAAAAAE5WRTWrDMBCFryKGLBJoY3fRjWIHeoiu6i6UZBwEY0VE49TB-O6VxrFTSih0qe_Ne_OjHpxpEDS8O7ZMeIAnqC1hAP3RA1990hKI_Fb55v06XJA4sZ0J-IjvT47RcYyBIuzP1vO2chVHHFjxiQ2pUr3k-SZRQlbBx_LIFoEN4zJfzQph_UMQr4hRXmBd456Xy5Uqt6pPKHmkfmzyPAZL2PCnbRpg8qWYu63I7lu4gswOSRYqrPNt3CgeqqzgbNwRK1A76w76YqjFspfcQTWmK3vJHlQm1puSTVSeqdOc_r9GaeCHfUSY26TXry6Br4RSK3C6yMEGT-aqVU3YbUZ2NF6rfP2KzXgbuYzY46czdgyazy0On_FlLH3F-UDXhgIO35UGlA1rAgAA)):
```svelte
<script>
let total = 100;
const total = 100;
let spent = $state(0);
let left = $state(total);
@ -297,32 +297,26 @@ You might be tempted to do something convoluted with effects to link one value t
</label>
```
Instead, use `oninput` callbacks or — better still — [function bindings](bind#Function-bindings) where possible ([demo](/playground/untitled#H4sIAAAAAAAAE51SsW6DMBT8FcvqABINdOhCIFKXTt06lg4GHpElYyz8iECIf69tcIIipo6-u3f3fPZMJWuBpvRzkBXyTpKSy5rLq6YRbbgATdOfmeKkrMgCBt9GPpQ66RsItFjJNBzhVScRJBobmumq5wovhSxQABLskAmSk7ckOXtMKyM22ItGhhAk4Z0R0OwIN-tIQzd-90HVhvy2HsGNiQFCMltBgd7XoecV2xzXNV7XaEcth7ZfRv7kujnsTX2Qd7USb5rFjwZkJlgJwpWRcakG04cpOS9oz-QVCuoeInXW-RyEJL-sG0b7Wy6kZWM-u7CFxM5tdrIl9qg72vB74H-y7T2iXROHyVb0CLanp1yNk4D1A1jQ91hzrQSbUtIIGLcir0ylJDm9Q7urz42bX4UwIk2xH2D5Xf4A7SeMcMQCAAA=)):
Instead, use `oninput` callbacks or — better still — [function bindings](bind#Function-bindings) where possible ([demo](/playground/untitled#H4sIAAAAAAAAE5VRvW7CMBB-FcvqECQK6dDFJEgsnfoGTQdDLsjSxVjxhYKivHvPBwFUsXS8774_nwftbQva6I_e78gdvNo6Xzu_j3quG4cQtfkaNJ1DIiWA8atkE8IiHgEpYVsb4Rm-O3gCT2yji7jrXKB15StiOJKiA1lUpXrL81VCEUjFwHTGXiJZgiyf3TYIjSxq6NwR6uyifr0ohMbEZnpHH2rWf7ImS8KZGtK6osl_UqelRIyVL5b3ir5AuwWUtoXzoee6fIWy0p31e6i0XMocLfZQDuI6qtaeykGcR7UU6XWznFAZU9LN_X9B2UyVayk9f3ji0-REugen6U9upDOCcAWcLlS7GNCejWoQTqsLtrfBqHzxDu3DrUTOf0xwIm2o62H85sk6_OHG2jQWI4y_3byXXGMCAAA=)):
```svelte
<script>
let total = 100;
const total = 100;
let spent = $state(0);
let left = $state(total);
function updateSpent(value) {
spent = value;
left = total - spent;
}
let left = $derived(total - spent);
function updateLeft(value) {
left = value;
+++ function updateLeft(left) {
spent = total - left;
}
}+++
</script>
<label>
<input type="range" bind:value={() => spent, updateSpent} max={total} />
<input type="range" bind:value={spent} max={total} />
{spent}/{total} spent
</label>
<label>
<input type="range" bind:value={() => left, updateLeft} max={total} />
<input type="range" +++bind:value={() => left, updateLeft}+++ max={total} />
{left}/{total} left
</label>
```

@ -82,12 +82,14 @@ As with elements, `name={name}` can be replaced with the `{name}` shorthand.
<Widget foo={bar} answer={42} text="hello" />
```
## Spread attributes
_Spread attributes_ allow many attributes or properties to be passed to an element or component at once.
An element or component can have multiple spread attributes, interspersed with regular ones.
An element or component can have multiple spread attributes, interspersed with regular ones. Order matters — if `things.a` exists it will take precedence over `a="b"`, while `c="d"` would take precedence over `things.c`:
```svelte
<Widget {...things} />
<Widget a="b" {...things} c="d" />
```
## Events

@ -43,7 +43,9 @@ An each block can also specify an _index_, equivalent to the second argument in
{#each expression as name, index (key)}...{/each}
```
If a _key_ expression is provided — which must uniquely identify each list item — Svelte will use it to diff the list when data changes, rather than adding or removing items at the end. The key can be any object, but strings and numbers are recommended since they allow identity to persist when the objects themselves change.
If a _key_ expression is provided — which must uniquely identify each list item — Svelte will use it to intelligently update the list when data changes by inserting, moving and deleting items, rather than adding or removing items at the end and updating the state in the middle.
The key can be any object, but strings and numbers are recommended since they allow identity to persist when the objects themselves change.
```svelte
{#each items as item (item.id)}

@ -0,0 +1,166 @@
---
title: {@attach ...}
---
Attachments are functions that run in an [effect]($effect) when an element is mounted to the DOM or when [state]($state) read inside the function updates.
Optionally, they can return a function that is called before the attachment re-runs, or after the element is later removed from the DOM.
> [!NOTE]
> Attachments are available in Svelte 5.29 and newer.
```svelte
<!--- file: App.svelte --->
<script>
/** @type {import('svelte/attachments').Attachment} */
function myAttachment(element) {
console.log(element.nodeName); // 'DIV'
return () => {
console.log('cleaning up');
};
}
</script>
<div {@attach myAttachment}>...</div>
```
An element can have any number of attachments.
## Attachment factories
A useful pattern is for a function, such as `tooltip` in this example, to _return_ an attachment ([demo](/playground/untitled#H4sIAAAAAAAAE3VT0XLaMBD8lavbDiaNCUlbHhTItG_5h5AH2T5ArdBppDOEMv73SkbGJGnH47F9t3un3TsfMyO3mInsh2SW1Sa7zlZKo8_E0zHjg42pGAjxBPxp7cTvUHOMldLjv-IVGUbDoUw295VTlh-WZslqa8kxsLL2ACtHWxh175NffnQfAAGikSGxYQGfPEvGfPSIWtOH0TiBVo2pWJEBJtKhQp4YYzjG9JIdcuMM5IZqHMPioY8vOSA997zQoevf4a7heO7cdp34olRiTGr07OhwH1IdoO2A7dLMbwahZq6MbRhKZWqxk7rBxTGVbuHmhCgb5qDgmIx_J6XtHHukHTrYYqx_YpzYng8aO4RYayql7hU-1ZJl0akqHBE_D9KLolwL-Dibzc7iSln9XjtqTF1UpMkJ2EmXR-BgQErsN4pxIJKr0RVO1qrxAqaTO4fbc9bKulZm3cfDY3aZDgvFGErWjmzhN7KmfX5rXyDeX8Pt1mU-hXjdBOrtuB97vK4GPUtmJ41XcRMEGDLD8do0nJ73zhUhSlyRw0t3vPqD8cjfLs-axiFgNBrkUd9Ulp50c-GLxlXAVlJX-ffpZyiSn7H0eLCUySZQcQdXlxj4El0Yv_FZvIKElqqGTruVLhzu7VRKCh22_5toOyxsWqLwwzK-cCbYNdg-hy-p9D7sbiZWUnts_wLUOF3CJgQAAA==)):
```svelte
<!--- file: App.svelte --->
<script>
import tippy from 'tippy.js';
let content = $state('Hello!');
/**
* @param {string} content
* @returns {import('svelte/attachments').Attachment}
*/
function tooltip(content) {
return (element) => {
const tooltip = tippy(element, { content });
return tooltip.destroy;
};
}
</script>
<input bind:value={content} />
<button {@attach tooltip(content)}>
Hover me
</button>
```
Since the `tooltip(content)` expression runs inside an [effect]($effect), the attachment will be destroyed and recreated whenever `content` changes. The same thing would happen for any state read _inside_ the attachment function when it first runs. (If this isn't what you want, see [Controlling when attachments re-run](#Controlling-when-attachments-re-run).)
## Inline attachments
Attachments can also be created inline ([demo](/playground/untitled#H4sIAAAAAAAAE71Wf3OaWBT9KoyTTnW3MS-I3dYmnWXVtnRAazRJzbozRSQEApiRhwKO333vuY8m225m_9yZGOT9OPfcc84D943UTfxGr_G7K6Xr3TVeNW7D2M8avT_3DVk-YAoDNF4vNB8e2tnWjyXGlm7mPzfurVPpp5JgGmeZtwkf5PtFupCxLzVvHa832rl2lElX-s2Xm2DZFNqp_hs-rZetd4v07ORpT3qmQHu7MF2td0BZp8k6z_xkvfXP902_pZ2_1_aYWEiqm0kN8I4r79qbdZ6umnq3q_2iNf22F4dE6qt2oimwdpim_uY6XMm7Fuo-IQT_iTD_CeGTHwZ38ieIJUFQRxirR1Xf39Dw0X5z0I72Af4tD61vvPNwWKQnqmfPTbduhsEd2J3vO_oBd3dc6fF2X7umNdWGf0vBRhSS6qoV7cCXfTXWfKmvWG61_si_vfU92Wz-E4RhsLhNIYinsox9QKGVd8-tuACCeKXRX12P-T_eKf7fhTq0Hvt-f3ailtSeoxJHRo1-58NoPe1UiBc1hkL8Yeh45y_vQ3mcuNl9T8s3cXPRWLnS7YWJG_gn2Tb4tUjid8jua-PVl08j_ab8I14mH8Llx0s5Tz5Err4ql52r_GYg0mVy1bEGZuD0ze64b5TWYFiM-16wSuJ4JT5vfVpDcztrcG_YkRU4s6HxufzDWF4XuVeJ1P10IbzBemt3Vp1V2e04ZXfrJd7Wicyd039brRIv_RIVu_nXi7X1cfL2sy66ztToUp1TO7qJ7NlwZ0f30pld5qNSVE5o6PbMojFHjgZB7oSicPpGteyLclQap7SvY0dXtM_LR1NT2JFHey3aaxa0VxCeYJ7RMHemoiCcgPZV9pR7o7kgcOjeGliYk9hjDZx8FAq6enwlTPSZj_vYPw9Il64dXdIY8ZmapzwfEd8-1ZyaxWhqkIZOibXUd-6Upqi1pD4uMicCV1GA_7zi73UN8BaF4sC8peJtMjfmjbHZBFwq5ov50qRaE0l96NZggnW4KqypYRAW-uhSz9ADvklwJF2J-5W0Z5fQPBhDX92R6I_0IFxRgDftge4l4dP-gH1hjD7uqU6fsOEZ9UNrCdPB-nys6uXgY6O3ZMd9sy5T9PghqrWHdjo4jB51CgLiKJaDYYA-7WgYONf1FbjkI-mE3EAfUY_rijfuJ_CVPaR50oe9JF7Q0pI8Dw3osxxYHdYPGbp2CnwHF8KvwJv2wEv0Z3ilQI6U9uwbZxbYJXvEmjjQjjCHkvNLvNg3yhzXQd1olamsT4IRrZmX0MUDpwL7R8zzHj7pSh9hPHFSHjLezKqAST51uC5zmtQ87skDUaneLokT5RbXkPWSYz53Abgjc8_o4KFGUZ-Hgv2Z1l5OTYM9D-HfUD0L-EwxH5wRnIG61gS-khfgY1bq7IAP_DA4l5xRuh9xlm8yGjutc8t-wHtkhWv3hc7aqGwiK5KzgvM5xRkZYn193uEln-su55j1GaIv7oM4iPrsVHiG0Dx7TR9-1lBfqFdwfvSd5LNL5xyZVp5NoHFZ57FkfiF6vKs4k5zvIfrX5xX6MXmt0gM5MTu8DjnhukrHHzTRd3jm0dma0_f_x5cxP9f4jBdqHvmbq2fUjzqcKh2Cp-yWj9ntcHanXmBXxhu7Q--eyjhfNFpaV7zgz4nWEUb7zUOhpevjjf_gu_KZ99pxFlZ-T3sttkmYqrco_26q35v0Ewzv5EZPbnL_8BfduWGMnyyN3q0bZ_7hb_7KG_L4CQAA)):
```svelte
<!--- file: App.svelte --->
<canvas
width={32}
height={32}
{@attach (canvas) => {
const context = canvas.getContext('2d');
$effect(() => {
context.fillStyle = color;
context.fillRect(0, 0, canvas.width, canvas.height);
});
}}
></canvas>
```
> [!NOTE]
> The nested effect runs whenever `color` changes, while the outer effect (where `canvas.getContext(...)` is called) only runs once, since it doesn't read any reactive state.
## Passing attachments to components
When used on a component, `{@attach ...}` will create a prop whose key is a [`Symbol`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol). If the component then [spreads](/tutorial/svelte/spread-props) props onto an element, the element will receive those attachments.
This allows you to create _wrapper components_ that augment elements ([demo](/playground/untitled#H4sIAAAAAAAAE3VUS3ObMBD-KxvajnFqsJM2PhA7TXrKob31FjITAbKtRkiMtDhJPfz3LiAMdpxhGJvdb1_fPnaeYjn3Iu-WIbJ04028lZDcetHDzsO3olbVApI74F1RhHbLJdayhFl-Sp5qhVwhufEWNjWiwJtYxSjyQhsEFEXxBiujcxg1_8O_dnQ9APwsEbVyiHDafjrvDZCgkiO4MLCEzxYZcn90z6XUZ6OxA61KlaIgV6i1pFC-sxjDrlbHaDiWRoGvdMbHsLzp5DES0mJnRxGaRBvcBHb7yFUTCQeunEWYcYtGv12TqgFUDbCK1WLaM6IWQhUlQiJUFm2ZLPly51xXMG0Rjoyd69C7UqqG2nu95QZyXvtvLVpri2-SN4hoLXXCZFfhQ8aQBU1VgdEaH_vSgyBZR_BpPp_vi0tY-rw2ulRZkGqpTQRbZvwa2BPgFC8bgbw31CbjJjAsE6WNYBZeGp7vtQXLMqHWnZx-5kM1TR5ycpkZXQR2wzL94l8Ur1C_3-g168SfQf1MyfRi3LW9fs77emJEw5QV9SREoLTq06tcczq7d6xEUcJX2vAhO1b843XK34e5unZEMBr15ekuKEusluWAF8lXhE2ZTP2r2RcIHJ-163FPKerCgYJLOB9i4GvNwviI5-gAQiFFBk3tBTOU3HFXEk0R8o86WvUD64aINhv5K3oRmpJXkw8uxMG6Hh6JY9X7OwGSqfUy9tDG3sHNoEi0d_d_fv9qndxRU0VClFqo3KVo3U655Hnt1PXB3Qra2Y2QGdEwgTAMCxopsoxOe6SD0gD8movDhT0LAnhqlE8gVCpLWnRoV7OJCkFAwEXitrYL1W7p7pbiE_P7XH6E_rihODm5s52XtiH9Ekaw0VgI9exadWL1uoEYjPtg2672k5szsxbKyWB2fdT0w5Y_0hcT8oXOlRetmLS8-g-6TLXXQgYAAA==)):
```svelte
<!--- file: Button.svelte --->
<script>
/** @type {import('svelte/elements').HTMLButtonAttributes} */
let { children, ...props } = $props();
</script>
<!-- `props` includes attachments -->
<button {...props}>
{@render children?.()}
</button>
```
```svelte
<!--- file: App.svelte --->
<script>
import tippy from 'tippy.js';
import Button from './Button.svelte';
let content = $state('Hello!');
/**
* @param {string} content
* @returns {import('svelte/attachments').Attachment}
*/
function tooltip(content) {
return (element) => {
const tooltip = tippy(element, { content });
return tooltip.destroy;
};
}
</script>
<input bind:value={content} />
<Button {@attach tooltip(content)}>
Hover me
</Button>
```
## Controlling when attachments re-run
Attachments, unlike [actions](use), are fully reactive: `{@attach foo(bar)}` will re-run on changes to `foo` _or_ `bar` (or any state read inside `foo`):
```js
// @errors: 7006 2304 2552
function foo(bar) {
return (node) => {
veryExpensiveSetupWork(node);
update(node, bar);
};
}
```
In the rare case that this is a problem (for example, if `foo` does expensive and unavoidable setup work) consider passing the data inside a function and reading it in a child effect:
```js
// @errors: 7006 2304 2552
function foo(+++getBar+++) {
return (node) => {
veryExpensiveSetupWork(node);
+++ $effect(() => {
update(node, getBar());
});+++
}
}
```
## Creating attachments programmatically
To add attachments to an object that will be spread onto a component or element, use [`createAttachmentKey`](svelte-attachments#createAttachmentKey).
## Converting actions to attachments
If you're using a library that only provides actions, you can convert them to attachments with [`fromAction`](svelte-attachments#fromAction), allowing you to (for example) use them with components.

@ -4,7 +4,7 @@ title: bind:
Data ordinarily flows down, from parent to child. The `bind:` directive allows data to flow the other way, from child to parent.
The general syntax is `bind:property={expression}`, where `expression` is an _lvalue_ (i.e. a variable or an object property). When the expression is an identifier with the same name as the property, we can omit the expression — in other words these are equivalent:
The general syntax is `bind:property={expression}`, where `expression` is an [_lvalue_](https://press.rebus.community/programmingfundamentals/chapter/lvalue-and-rvalue/) (i.e. a variable or an object property). When the expression is an identifier with the same name as the property, we can omit the expression — in other words these are equivalent:
<!-- prettier-ignore -->
```svelte
@ -117,28 +117,52 @@ Since 5.6.0, if an `<input>` has a `defaultChecked` attribute and is part of a f
</form>
```
## `<input bind:indeterminate>`
Checkboxes can be in an [indeterminate](https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/indeterminate) state, independently of whether they are checked or unchecked:
```svelte
<script>
let checked = $state(false);
let indeterminate = $state(true);
</script>
<form>
<input type="checkbox" bind:checked bind:indeterminate>
{#if indeterminate}
waiting...
{:else if checked}
checked
{:else}
unchecked
{/if}
</form>
```
## `<input bind:group>`
Inputs that work together can use `bind:group`.
Inputs that work together can use `bind:group` ([demo](/playground/untitled#H4sIAAAAAAAAE62T32_TMBDH_5XDQkpbrct7SCMGEvCEECDxsO7BSW6L2c227EvbKOv_jp0f6jYhQKJv5_P3PvdL1wstH1Bk4hMSGdgbRzUssFaM9VJciFtF6EV23QvubNRFR_BPUVfWXvodEkdfKT3-zl8Zzag5YETuK6csF1u9ZUIGNo4VkYQNvPYsGRfJF5JKJ8s3QRJE6WoFb2Nq6K-ck13u2Sl9Vxxhlc6QUBIFnz9Brm9ifJ6esun81XoNd860FmtwslYGlLYte5AO4aHlVhJ1gIeKWq92COt1iMtJlkhFPkgh1rHZiiF6K6BUus4G5KafGznCTlIbVUMfQZUWMJh5OrL-C_qjMYSwb1DyiH7iOEuCb1ZpWTUjfHqcwC_GWDVY3ZfmME_SGttSmD9IHaYatvWHIc6xLyqad3mq6KuqcCwnWn9p8p-p71BqP2IH81zc9w2in-od7XORP7ayCpd5YCeXI_-p59mObPF9WmwGpx3nqS2Gzw8TO3zOaS5_GqUXyQUkS3h8hOSz0ZhMESHGc0c4Hm3MAn00t1wrb0l2GZRkqvt4sXwczm6Qh8vnUJzI2LV4vAkvqWgfehTZrSSPx19WiVfFfAQAAA==)):
```svelte
<!--- file: BurritoChooser.svelte --->
<script>
let tortilla = $state('Plain');
/** @type {Array<string>} */
/** @type {string[]} */
let fillings = $state([]);
</script>
<!-- grouped radio inputs are mutually exclusive -->
<input type="radio" bind:group={tortilla} value="Plain" />
<input type="radio" bind:group={tortilla} value="Whole wheat" />
<input type="radio" bind:group={tortilla} value="Spinach" />
<label><input type="radio" bind:group={tortilla} value="Plain" /> Plain</label>
<label><input type="radio" bind:group={tortilla} value="Whole wheat" /> Whole wheat</label>
<label><input type="radio" bind:group={tortilla} value="Spinach" /> Spinach</label>
<!-- grouped checkbox inputs populate an array -->
<input type="checkbox" bind:group={fillings} value="Rice" />
<input type="checkbox" bind:group={fillings} value="Beans" />
<input type="checkbox" bind:group={fillings} value="Cheese" />
<input type="checkbox" bind:group={fillings} value="Guac (extra)" />
<label><input type="checkbox" bind:group={fillings} value="Rice" /> Rice</label>
<label><input type="checkbox" bind:group={fillings} value="Beans" /> Beans</label>
<label><input type="checkbox" bind:group={fillings} value="Cheese" /> Cheese</label>
<label><input type="checkbox" bind:group={fillings} value="Guac (extra)" /> Guac (extra)</label>
```
> [!NOTE] `bind:group` only works if the inputs are in the same Svelte component.
@ -227,6 +251,7 @@ You can give the `<select>` a default value by adding a `selected` attribute to
- [`seeking`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/seeking_event)
- [`ended`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/ended)
- [`readyState`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/readyState)
- [`played`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/played)
```svelte
<audio src={clip} bind:duration bind:currentTime bind:paused></audio>
@ -254,6 +279,10 @@ You can give the `<select>` a default value by adding a `selected` attribute to
</details>
```
## `window` and `document`
To bind to properties of `window` and `document`, see [`<svelte:window>`](svelte-window) and [`<svelte:document>`](svelte-document).
## Contenteditable bindings
Elements with the `contenteditable` attribute support the following bindings:
@ -278,6 +307,10 @@ All visible elements have the following readonly bindings, measured with a `Resi
- [`clientHeight`](https://developer.mozilla.org/en-US/docs/Web/API/Element/clientHeight)
- [`offsetWidth`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/offsetWidth)
- [`offsetHeight`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/offsetHeight)
- [`contentRect`](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserverEntry/contentRect)
- [`contentBoxSize`](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserverEntry/contentBoxSize)
- [`borderBoxSize`](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserverEntry/borderBoxSize)
- [`devicePixelContentBoxSize`](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserverEntry/devicePixelContentBoxSize)
```svelte
<div bind:offsetWidth={width} bind:offsetHeight={height}>
@ -285,7 +318,7 @@ All visible elements have the following readonly bindings, measured with a `Resi
</div>
```
> [!NOTE] `display: inline` elements do not have a width or height (except for elements with 'intrinsic' dimensions, like `<img>` and `<canvas>`), and cannot be observed with a `ResizeObserver`. You will need to change the `display` style of these elements to something else, such as `inline-block`.
> [!NOTE] `display: inline` elements do not have a width or height (except for elements with 'intrinsic' dimensions, like `<img>` and `<canvas>`), and cannot be observed with a `ResizeObserver`. You will need to change the `display` style of these elements to something else, such as `inline-block`. Note that CSS transformations do not trigger `ResizeObserver` callbacks.
## bind:this

@ -2,6 +2,9 @@
title: use:
---
> [!NOTE]
> In Svelte 5.29 and newer, consider using [attachments](@attach) instead, as they are more flexible and composable.
Actions are functions that are called when an element is mounted. They are added with the `use:` directive, and will typically use an `$effect` so that they can reset any state when the element is unmounted:
```svelte

@ -34,8 +34,10 @@ To mark a style as important, use the `|important` modifier:
<div style:color|important="red">...</div>
```
When `style:` directives are combined with `style` attributes, the directives will take precedence:
When `style:` directives are combined with `style` attributes, the directives will take precedence,
even over `!important` properties:
```svelte
<div style="color: blue;" style:color="red">This will be red</div>
<div style:color="red" style="color: blue">This will be red</div>
<div style:color="red" style="color: blue !important">This will still be red</div>
```

@ -8,7 +8,7 @@ title: <svelte:body>
Similarly to `<svelte:window>`, this element allows you to add listeners to events on `document.body`, such as `mouseenter` and `mouseleave`, which don't fire on `window`. It also lets you use [actions](use) on the `<body>` element.
As with `<svelte:window>` and `<svelte:document>`, this element may only appear the top level of your component and must never be inside a block or element.
As with `<svelte:window>` and `<svelte:document>`, this element may only appear at the top level of your component and must never be inside a block or element.
```svelte
<svelte:body onmouseenter={handleMouseenter} onmouseleave={handleMouseleave} use:someAction />

@ -125,7 +125,7 @@ In many cases this is perfectly fine, but there is a risk: if you mutate the sta
```svelte
<!--- file: App.svelte ---->
<script>
import { myGlobalState } from 'svelte';
import { myGlobalState } from './state.svelte.js';
let { data } = $props();

@ -6,9 +6,9 @@ Testing helps you write and maintain your code and guard against regressions. Te
## Unit and integration testing using Vitest
Unit tests allow you to test small isolated parts of your code. Integration tests allow you to test parts of your application to see if they work together. If you're using Vite (including via SvelteKit), we recommend using [Vitest](https://vitest.dev/).
Unit tests allow you to test small isolated parts of your code. Integration tests allow you to test parts of your application to see if they work together. If you're using Vite (including via SvelteKit), we recommend using [Vitest](https://vitest.dev/). You can use the Svelte CLI to [setup Vitest](/docs/cli/vitest) either during project creation or later on.
To get started, install Vitest:
To setup Vitest manually, first install it:
```bash
npm install -D vitest
@ -254,9 +254,9 @@ When writing component tests that involve two-way bindings, context or snippet p
E2E (short for 'end to end') tests allow you to test your full application through the eyes of the user. This section uses [Playwright](https://playwright.dev/) as an example, but you can also use other solutions like [Cypress](https://www.cypress.io/) or [NightwatchJS](https://nightwatchjs.org/).
To get started with Playwright, either install it via [the VS Code extension](https://playwright.dev/docs/getting-started-vscode), or install it from the command line using `npm init playwright`. It is also part of the setup CLI when you run `npx sv create`.
You can use the Svelte CLI to [setup Playwright](/docs/cli/playwright) either during project creation or later on. You can also [set it up with `npm init playwright`](https://playwright.dev/docs/intro). Additionally, you may also want to install an IDE plugin such as [the VS Code extension](https://playwright.dev/docs/getting-started-vscode) to be able to execute tests from inside your IDE.
After you've done that, you should have a `tests` folder and a Playwright config. You may need to adjust that config to tell Playwright what to do before running the tests - mainly starting your application at a certain port:
If you've run `npm init playwright` or are not using Vite, you may need to adjust the Playwright config to tell Playwright what to do before running the tests - mainly starting your application at a certain port. For example:
```js
/// file: playwright.config.js

@ -114,6 +114,8 @@ When constructing a custom element, you can tailor several aspects by defining `
...
```
> [!NOTE] While Typescript is supported in the `extend` function, it is subject to limitations: you need to set `lang="ts"` on one of the scripts AND you can only use [erasable syntax](https://www.typescriptlang.org/tsconfig/#erasableSyntaxOnly) in it. They are not processed by script preprocessors.
## Caveats and limitations
Custom elements can be a useful way to package components for consumption in a non-Svelte app, as they will work with vanilla HTML and JavaScript as well as [most frameworks](https://custom-elements-everywhere.com/). There are, however, some important differences to be aware of:

@ -833,9 +833,9 @@ Svelte 5 is more strict about the HTML structure and will throw a compiler error
Assignments to destructured parts of a `@const` declaration are no longer allowed. It was an oversight that this was ever allowed.
### :is(...) and :where(...) are scoped
### :is(...), :has(...), and :where(...) are scoped
Previously, Svelte did not analyse selectors inside `:is(...)` and `:where(...)`, effectively treating them as global. Svelte 5 analyses them in the context of the current component. As such, some selectors may now be treated as unused if they were relying on this treatment. To fix this, use `:global(...)` inside the `:is(...)/:where(...)` selectors.
Previously, Svelte did not analyse selectors inside `:is(...)`, `:has(...)`, and `:where(...)`, effectively treating them as global. Svelte 5 analyses them in the context of the current component. As such, some selectors may now be treated as unused if they were relying on this treatment. To fix this, use `:global(...)` inside the `:is(...)/:has(...)/:where(...)` selectors.
When using Tailwind's `@apply` directive, add a `:global` selector to preserve rules that use Tailwind-generated `:is(...)` selectors:

@ -81,9 +81,10 @@ _End-to-End Tests_: To ensure your users are able to interact with your applicat
Some resources for getting started with testing:
- [Svelte docs on testing](/docs/svelte/testing)
- [Setup Vitest using the Svelte CLI](/docs/cli/vitest)
- [Svelte Testing Library](https://testing-library.com/docs/svelte-testing-library/example/)
- [Svelte Component Testing in Cypress](https://docs.cypress.io/guides/component-testing/svelte/overview)
- [Example using vitest](https://github.com/vitest-dev/vitest/tree/main/examples/sveltekit)
- [Example using uvu test runner with JSDOM](https://github.com/lukeed/uvu/tree/master/examples/svelte)
- [Test Svelte components using Vitest & Playwright](https://davipon.hashnode.dev/test-svelte-component-using-vitest-playwright)
- [Component testing with WebdriverIO](https://webdriver.io/docs/component-testing/svelte)

@ -200,6 +200,19 @@ Consider the following code:
To fix it, either create callback props to communicate changes, or mark `person` as [`$bindable`]($bindable).
### select_multiple_invalid_value
```
The `value` property of a `<select multiple>` element should be an array, but it received a non-array value. The selection will be kept as is.
```
When using `<select multiple value={...}>`, Svelte will mark all selected `<option>` elements as selected by iterating over the array passed to `value`. If `value` is not an array, Svelte will emit this warning and keep the selected options as they are.
To silence the warning, ensure that `value`:
- is an array for an explicit selection
- is `null` or `undefined` to keep the selection as is
### state_proxy_equality_mismatch
```

@ -846,6 +846,38 @@ Cannot reassign or bind to snippet parameter
This snippet is shadowing the prop `%prop%` with the same name
```
### state_field_duplicate
```
`%name%` has already been declared on this class
```
An assignment to a class field that uses a `$state` or `$derived` rune is considered a _state field declaration_. The declaration can happen in the class body...
```js
class Counter {
count = $state(0);
}
```
...or inside the constructor...
```js
class Counter {
constructor() {
this.count = $state(0);
}
}
```
...but it can only happen once.
### state_field_invalid_assignment
```
Cannot assign to a state field before its declaration
```
### state_invalid_export
```
@ -855,7 +887,7 @@ Cannot export state from a module if it is reassigned. Either export a function
### state_invalid_placement
```
`%rune%(...)` can only be used as a variable declaration initializer or a class field
`%rune%(...)` can only be used as a variable declaration initializer, a class field declaration, or the first assignment to a class field at the top level of the constructor.
```
### store_invalid_scoped_subscription

@ -586,6 +586,14 @@ Attributes should not contain ':' characters to prevent ambiguity with Svelte di
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
```
### bidirectional_control_characters
```
A bidirectional control character was detected in your code. These characters can be used to alter the visual direction of your code and could have unintended consequences
```
Bidirectional control characters can alter the direction in which text appears to be in. For example, via control characters, you can make `defabc` look like `abcdef`. As a result, if you were to unknowingly copy and paste some code that has these control characters, they may alter the behavior of your code in ways you did not intend. See [trojansource.codes](https://trojansource.codes/) for more information.
### bind_invalid_each_rest
```
@ -624,6 +632,31 @@ In some situations a selector may target an element that is not 'visible' to the
</style>
```
### custom_element_props_identifier
```
Using a rest element or a non-destructured declaration with `$props()` means that Svelte can't infer what properties to expose when creating a custom element. Consider destructuring all the props or explicitly specifying the `customElement.props` option.
```
### element_implicitly_closed
```
This element is implicitly closed by the following `%tag%`, which can cause an unexpected DOM structure. Add an explicit `%closing%` to avoid surprises.
```
In HTML, some elements are implicitly closed by another element. For example, you cannot nest a `<p>` inside another `<p>`:
```html
<!-- this HTML... -->
<p><p>hello</p>
<!-- results in this DOM structure -->
<p></p>
<p>hello</p>
```
Similarly, a parent element's closing tag will implicitly close all child elements, even if the `</` was a typo and you meant to create a _new_ element. To avoid ambiguity, it's always a good idea to have an explicit closing tag.
### element_invalid_self_closing_tag
```

@ -0,0 +1,5 @@
---
title: svelte/attachments
---
> MODULE: svelte/attachments

@ -15,13 +15,10 @@
},
"scripts": {
"build": "pnpm -r --filter=./packages/* build",
"build:sites": "pnpm -r --filter=./sites/* build",
"preview-site": "npm run build --prefix sites/svelte-5-preview",
"check": "cd packages/svelte && pnpm build && cd ../../ && pnpm -r check",
"lint": "eslint && prettier --check .",
"format": "prettier --write .",
"test": "vitest run",
"test-output": "vitest run --coverage --reporter=json --outputFile=sites/svelte-5-preview/src/routes/status/results.json",
"changeset:version": "changeset version && pnpm -r generate:version && git add --all",
"changeset:publish": "changeset publish",
"bench": "node --allow-natives-syntax ./benchmarking/run.js",

@ -1,5 +1,297 @@
# svelte
## 5.34.3
### Patch Changes
- fix: don't eagerly execute deriveds on resume ([#16150](https://github.com/sveltejs/svelte/pull/16150))
- fix: prevent memory leaking signals in legacy mode ([#16145](https://github.com/sveltejs/svelte/pull/16145))
- fix: don't define `error.message` if it's not configurable ([#16149](https://github.com/sveltejs/svelte/pull/16149))
## 5.34.2
### Patch Changes
- fix: add missing typings for some dimension bindings ([#16142](https://github.com/sveltejs/svelte/pull/16142))
- fix: prune typescript class field declarations ([#16154](https://github.com/sveltejs/svelte/pull/16154))
## 5.34.1
### Patch Changes
- fix: correctly tag private class state fields ([#16132](https://github.com/sveltejs/svelte/pull/16132))
## 5.34.0
### Minor Changes
- feat: add source name logging to `$inspect.trace` ([#16060](https://github.com/sveltejs/svelte/pull/16060))
### Patch Changes
- fix: add `command` and `commandfor` to `HTMLButtonAttributes` ([#16117](https://github.com/sveltejs/svelte/pull/16117))
- fix: better `$inspect.trace()` output ([#16131](https://github.com/sveltejs/svelte/pull/16131))
- fix: properly hydrate dynamic css props components and remove element removal ([#16118](https://github.com/sveltejs/svelte/pull/16118))
## 5.33.19
### Patch Changes
- fix: reset `is_flushing` if `flushSync` is called and there's no scheduled effect ([#16119](https://github.com/sveltejs/svelte/pull/16119))
## 5.33.18
### Patch Changes
- chore: bump `esrap` dependency ([#16106](https://github.com/sveltejs/svelte/pull/16106))
- fix: destructuring state in ssr ([#16102](https://github.com/sveltejs/svelte/pull/16102))
## 5.33.17
### Patch Changes
- chore: update acorn parser `ecmaVersion` to parse import attributes ([#16098](https://github.com/sveltejs/svelte/pull/16098))
## 5.33.16
### Patch Changes
- fix: visit expression when destructuring state declarations ([#16081](https://github.com/sveltejs/svelte/pull/16081))
- fix: move xmlns attribute from SVGAttributes to to DOMAttributes ([#16080](https://github.com/sveltejs/svelte/pull/16080))
## 5.33.15
### Patch Changes
- fix: invoke parent boundary of deriveds that throw ([#16091](https://github.com/sveltejs/svelte/pull/16091))
## 5.33.14
### Patch Changes
- Revert "feat: enable TS autocomplete for Svelte HTML element definitions" ([#16063](https://github.com/sveltejs/svelte/pull/16063))
- fix: destructuring snippet arguments ([#16068](https://github.com/sveltejs/svelte/pull/16068))
## 5.33.13
### Patch Changes
- fix: avoid recursion error in `EachBlock` visitor ([#16058](https://github.com/sveltejs/svelte/pull/16058))
## 5.33.12
### Patch Changes
- fix: correctly transform reassignments to class fields in SSR mode ([#16051](https://github.com/sveltejs/svelte/pull/16051))
## 5.33.11
### Patch Changes
- fix: treat transitive dependencies of each blocks as mutable in legacy mode if item is mutated ([#16038](https://github.com/sveltejs/svelte/pull/16038))
## 5.33.10
### Patch Changes
- fix: use `fill: 'forwards'` on transition animations to prevent flicker ([#16035](https://github.com/sveltejs/svelte/pull/16035))
## 5.33.9
### Patch Changes
- fix: put expressions in effects unless known to be static ([#15792](https://github.com/sveltejs/svelte/pull/15792))
## 5.33.8
### Patch Changes
- fix: only `select_option` if `'value'` is in `next` ([#16032](https://github.com/sveltejs/svelte/pull/16032))
## 5.33.7
### Patch Changes
- fix: `bind:value` to select with stores ([#16028](https://github.com/sveltejs/svelte/pull/16028))
## 5.33.6
### Patch Changes
- fix: falsy attachments on components ([#16021](https://github.com/sveltejs/svelte/pull/16021))
- fix: correctly mark <option> elements as selected during SSR ([#16017](https://github.com/sveltejs/svelte/pull/16017))
## 5.33.5
### Patch Changes
- fix: handle derived destructured iterators ([#16015](https://github.com/sveltejs/svelte/pull/16015))
- fix: avoid rerunning attachments when unrelated spread attributes change ([#15961](https://github.com/sveltejs/svelte/pull/15961))
## 5.33.4
### Patch Changes
- fix: narrow `defaultChecked` to boolean ([#16009](https://github.com/sveltejs/svelte/pull/16009))
- fix: warn when using rest or identifier in custom elements without props option ([#16003](https://github.com/sveltejs/svelte/pull/16003))
## 5.33.3
### Patch Changes
- fix: allow using typescript in `customElement.extend` option ([#16001](https://github.com/sveltejs/svelte/pull/16001))
- fix: cleanup event handlers on media elements ([#16005](https://github.com/sveltejs/svelte/pull/16005))
## 5.33.2
### Patch Changes
- fix: correctly parse escaped unicode characters in css selector ([#15976](https://github.com/sveltejs/svelte/pull/15976))
- fix: don't mark deriveds as clean if updating during teardown ([#15997](https://github.com/sveltejs/svelte/pull/15997))
## 5.33.1
### Patch Changes
- fix: make deriveds on the server lazy again ([#15964](https://github.com/sveltejs/svelte/pull/15964))
## 5.33.0
### Minor Changes
- feat: XHTML compliance ([#15538](https://github.com/sveltejs/svelte/pull/15538))
- feat: add `fragments: 'html' | 'tree'` option for wider CSP compliance ([#15538](https://github.com/sveltejs/svelte/pull/15538))
## 5.32.2
### Patch Changes
- chore: simplify `<pre>` cleaning ([#15980](https://github.com/sveltejs/svelte/pull/15980))
- fix: attach `__svelte_meta` correctly to elements following a CSS wrapper ([#15982](https://github.com/sveltejs/svelte/pull/15982))
- fix: import untrack directly from client in `svelte/attachments` ([#15974](https://github.com/sveltejs/svelte/pull/15974))
## 5.32.1
### Patch Changes
- Warn when an invalid `<select multiple>` value is given ([#14816](https://github.com/sveltejs/svelte/pull/14816))
## 5.32.0
### Minor Changes
- feat: warn on implicitly closed tags ([#15932](https://github.com/sveltejs/svelte/pull/15932))
- feat: attachments `fromAction` utility ([#15933](https://github.com/sveltejs/svelte/pull/15933))
### Patch Changes
- fix: only re-run directly applied attachment if it changed ([#15962](https://github.com/sveltejs/svelte/pull/15962))
## 5.31.1
### Patch Changes
- fix: avoid auto-parenthesis for special-keywords-only `MediaQuery` ([#15937](https://github.com/sveltejs/svelte/pull/15937))
## 5.31.0
### Minor Changes
- feat: allow state fields to be declared inside class constructors ([#15820](https://github.com/sveltejs/svelte/pull/15820))
### Patch Changes
- fix: Add missing `AttachTag` in `Tag` union type inside the `AST` namespace from `"svelte/compiler"` ([#15946](https://github.com/sveltejs/svelte/pull/15946))
## 5.30.2
### Patch Changes
- fix: falsy attachments types ([#15939](https://github.com/sveltejs/svelte/pull/15939))
- fix: handle more hydration mismatches ([#15851](https://github.com/sveltejs/svelte/pull/15851))
## 5.30.1
### Patch Changes
- fix: add `typeParams` to `SnippetBlock` for legacy parser ([#15921](https://github.com/sveltejs/svelte/pull/15921))
## 5.30.0
### Minor Changes
- feat: allow generics on snippets ([#15915](https://github.com/sveltejs/svelte/pull/15915))
## 5.29.0
### Minor Changes
- feat: attachments ([#15000](https://github.com/sveltejs/svelte/pull/15000))
## 5.28.7
### Patch Changes
- fix: remove unncessary guards that require CSP privilege when removing event attributes ([#15846](https://github.com/sveltejs/svelte/pull/15846))
- fix: rewrite destructuring logic to handle iterators ([#15813](https://github.com/sveltejs/svelte/pull/15813))
## 5.28.6
### Patch Changes
- fix: use `transform.read` for `ownership_validator.mutation` array ([#15848](https://github.com/sveltejs/svelte/pull/15848))
- fix: don't redeclare `$slots` ([#15849](https://github.com/sveltejs/svelte/pull/15849))
## 5.28.5
### Patch Changes
- fix: proxify the value in assignment shorthands to the private field ([#15862](https://github.com/sveltejs/svelte/pull/15862))
- fix: more frequently update `bind:buffered` to actual value ([#15874](https://github.com/sveltejs/svelte/pull/15874))
## 5.28.4
### Patch Changes
- fix: treat nullish expression as empty string ([#15901](https://github.com/sveltejs/svelte/pull/15901))
- fix: prevent invalid BigInt calls from blowing up at compile time ([#15900](https://github.com/sveltejs/svelte/pull/15900))
- fix: warn on bidirectional control characters ([#15893](https://github.com/sveltejs/svelte/pull/15893))
- fix: emit right error for a shadowed invalid rune ([#15892](https://github.com/sveltejs/svelte/pull/15892))
## 5.28.3
### Patch Changes
- chore: avoid microtasks when flushing sync ([#15895](https://github.com/sveltejs/svelte/pull/15895))
- fix: improve error message for migration errors when slot would be renamed ([#15841](https://github.com/sveltejs/svelte/pull/15841))
- fix: allow characters in the supplementary special-purpose plane ([#15823](https://github.com/sveltejs/svelte/pull/15823))
## 5.28.2
### Patch Changes

@ -31,6 +31,8 @@
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
// TypeScript Version: 2.8
import type { Attachment } from 'svelte/attachments';
// Note: We also allow `null` as a valid value because Svelte treats this the same as `undefined`
type Booleanish = boolean | 'true' | 'false';
@ -461,6 +463,8 @@ export interface DOMAttributes<T extends EventTarget> {
'on:fullscreenerror'?: EventHandler<Event, T> | undefined | null;
onfullscreenerror?: EventHandler<Event, T> | undefined | null;
onfullscreenerrorcapture?: EventHandler<Event, T> | undefined | null;
xmlns?: string | undefined | null;
}
// All the WAI-ARIA 1.1 attributes from https://www.w3.org/TR/wai-aria-1.1/
@ -840,6 +844,10 @@ export interface HTMLAttributes<T extends EventTarget> extends AriaAttributes, D
readonly 'bind:borderBoxSize'?: Array<ResizeObserverSize> | undefined | null;
readonly 'bind:devicePixelContentBoxSize'?: Array<ResizeObserverSize> | undefined | null;
readonly 'bind:focused'?: boolean | undefined | null;
readonly 'bind:clientWidth'?: number | undefined | null;
readonly 'bind:clientHeight'?: number | undefined | null;
readonly 'bind:offsetWidth'?: number | undefined | null;
readonly 'bind:offsetHeight'?: number | undefined | null;
// SvelteKit
'data-sveltekit-keepfocus'?: true | '' | 'off' | undefined | null;
@ -860,6 +868,9 @@ export interface HTMLAttributes<T extends EventTarget> extends AriaAttributes, D
// allow any data- attribute
[key: `data-${string}`]: any;
// allow any attachment and falsy values (by using false we prevent the usage of booleans values by themselves)
[key: symbol]: Attachment<T> | false | undefined | null;
}
export type HTMLAttributeAnchorTarget = '_self' | '_blank' | '_parent' | '_top' | (string & {});
@ -919,6 +930,17 @@ export interface HTMLButtonAttributes extends HTMLAttributes<HTMLButtonElement>
value?: string | string[] | number | undefined | null;
popovertarget?: string | undefined | null;
popovertargetaction?: 'toggle' | 'show' | 'hide' | undefined | null;
command?:
| 'show-modal'
| 'close'
| 'request-close'
| 'show-popover'
| 'hide-popover'
| 'toggle-popover'
| (string & {})
| undefined
| null;
commandfor?: string | undefined | null;
}
export interface HTMLCanvasAttributes extends HTMLAttributes<HTMLCanvasElement> {
@ -1110,8 +1132,8 @@ export interface HTMLInputAttributes extends HTMLAttributes<HTMLInputElement> {
// needs both casing variants because language tools does lowercase names of non-shorthand attributes
defaultValue?: any;
defaultvalue?: any;
defaultChecked?: any;
defaultchecked?: any;
defaultChecked?: boolean | undefined | null;
defaultchecked?: boolean | undefined | null;
width?: number | string | undefined | null;
webkitdirectory?: boolean | undefined | null;
@ -1804,7 +1826,6 @@ export interface SVGAttributes<T extends EventTarget> extends AriaAttributes, DO
'xlink:type'?: string | undefined | null;
'xml:base'?: string | undefined | null;
'xml:lang'?: string | undefined | null;
xmlns?: string | undefined | null;
'xmlns:xlink'?: string | undefined | null;
'xml:space'?: string | undefined | null;
y1?: number | string | undefined | null;

@ -168,6 +168,17 @@ Consider the following code:
To fix it, either create callback props to communicate changes, or mark `person` as [`$bindable`]($bindable).
## select_multiple_invalid_value
> The `value` property of a `<select multiple>` element should be an array, but it received a non-array value. The selection will be kept as is.
When using `<select multiple value={...}>`, Svelte will mark all selected `<option>` elements as selected by iterating over the array passed to `value`. If `value` is not an array, Svelte will emit this warning and keep the selected options as they are.
To silence the warning, ensure that `value`:
- is an array for an explicit selection
- is `null` or `undefined` to keep the selection as is
## state_proxy_equality_mismatch
> Reactive `$state(...)` proxies and the values they proxy have different identities. Because of this, comparisons with `%operator%` will produce unexpected results

@ -212,13 +212,41 @@ It's possible to export a snippet from a `<script module>` block, but only if it
> Cannot reassign or bind to snippet parameter
## state_field_duplicate
> `%name%` has already been declared on this class
An assignment to a class field that uses a `$state` or `$derived` rune is considered a _state field declaration_. The declaration can happen in the class body...
```js
class Counter {
count = $state(0);
}
```
...or inside the constructor...
```js
class Counter {
constructor() {
this.count = $state(0);
}
}
```
...but it can only happen once.
## state_field_invalid_assignment
> Cannot assign to a state field before its declaration
## state_invalid_export
> Cannot export state from a module if it is reassigned. Either export a function returning the state value or only mutate the state value's properties
## state_invalid_placement
> `%rune%(...)` can only be used as a variable declaration initializer or a class field
> `%rune%(...)` can only be used as a variable declaration initializer, a class field declaration, or the first assignment to a class field at the top level of the constructor.
## store_invalid_scoped_subscription

@ -1,3 +1,9 @@
## bidirectional_control_characters
> A bidirectional control character was detected in your code. These characters can be used to alter the visual direction of your code and could have unintended consequences
Bidirectional control characters can alter the direction in which text appears to be in. For example, via control characters, you can make `defabc` look like `abcdef`. As a result, if you were to unknowingly copy and paste some code that has these control characters, they may alter the behavior of your code in ways you did not intend. See [trojansource.codes](https://trojansource.codes/) for more information.
## legacy_code
> `%code%` is no longer valid — please use `%suggestion%` instead

@ -1,3 +1,7 @@
## custom_element_props_identifier
> Using a rest element or a non-destructured declaration with `$props()` means that Svelte can't infer what properties to expose when creating a custom element. Consider destructuring all the props or explicitly specifying the `customElement.props` option.
## export_let_unused
> Component has unused export property '%name%'. If it is for external reference only, please consider using `export const %name%`

@ -30,6 +30,23 @@
> `<%name%>` will be treated as an HTML element unless it begins with a capital letter
## element_implicitly_closed
> This element is implicitly closed by the following `%tag%`, which can cause an unexpected DOM structure. Add an explicit `%closing%` to avoid surprises.
In HTML, some elements are implicitly closed by another element. For example, you cannot nest a `<p>` inside another `<p>`:
```html
<!-- this HTML... -->
<p><p>hello</p>
<!-- results in this DOM structure -->
<p></p>
<p>hello</p>
```
Similarly, a parent element's closing tag will implicitly close all child elements, even if the `</` was a typo and you meant to create a _new_ element. To avoid ambiguity, it's always a good idea to have an explicit closing tag.
## element_invalid_self_closing_tag
> Self-closing HTML tags for non-void elements are ambiguous — use `<%name% ...></%name%>` rather than `<%name% ... />`

@ -2,7 +2,7 @@
"name": "svelte",
"description": "Cybernetically enhanced web apps",
"license": "MIT",
"version": "5.28.2",
"version": "5.34.3",
"type": "module",
"types": "./types/index.d.ts",
"engines": {
@ -34,6 +34,10 @@
"types": "./types/index.d.ts",
"default": "./src/animate/index.js"
},
"./attachments": {
"types": "./types/index.d.ts",
"default": "./src/attachments/index.js"
},
"./compiler": {
"types": "./types/index.d.ts",
"require": "./compiler/index.js",
@ -132,7 +136,7 @@
],
"scripts": {
"build": "node scripts/process-messages && rollup -c && pnpm generate:types && node scripts/check-treeshakeability.js",
"dev": "node scripts/process-messages && rollup -cw",
"dev": "node scripts/process-messages -w & rollup -cw",
"check": "tsc --project tsconfig.runtime.json && tsc && cd ./tests/types && tsc",
"check:watch": "tsc --watch",
"generate:version": "node ./scripts/generate-version.js",
@ -167,7 +171,7 @@
"axobject-query": "^4.1.0",
"clsx": "^2.1.1",
"esm-env": "^1.2.1",
"esrap": "^1.4.6",
"esrap": "^1.4.8",
"is-reference": "^3.0.3",
"locate-character": "^3.0.0",
"magic-string": "^0.30.11",

@ -35,6 +35,7 @@ await createBundle({
[pkg.name]: `${dir}/src/index.d.ts`,
[`${pkg.name}/action`]: `${dir}/src/action/public.d.ts`,
[`${pkg.name}/animate`]: `${dir}/src/animate/public.d.ts`,
[`${pkg.name}/attachments`]: `${dir}/src/attachments/public.d.ts`,
[`${pkg.name}/compiler`]: `${dir}/src/compiler/public.d.ts`,
[`${pkg.name}/easing`]: `${dir}/src/easing/index.js`,
[`${pkg.name}/legacy`]: `${dir}/src/legacy/legacy-client.js`,

@ -1,14 +1,19 @@
// @ts-check
import process from 'node:process';
import fs from 'node:fs';
import * as acorn from 'acorn';
import { walk } from 'zimmerframe';
import * as esrap from 'esrap';
const DIR = '../../documentation/docs/98-reference/.generated';
const watch = process.argv.includes('-w');
function run() {
/** @type {Record<string, Record<string, { messages: string[], details: string | null }>>} */
const messages = {};
const seen = new Set();
const DIR = '../../documentation/docs/98-reference/.generated';
fs.rmSync(DIR, { force: true, recursive: true });
fs.mkdirSync(DIR);
@ -54,6 +59,7 @@ for (const category of fs.readdirSync('messages')) {
}
sorted.sort((a, b) => (a.code < b.code ? -1 : 1));
fs.writeFileSync(
`messages/${category}/${file}`,
sorted.map((x) => x._.trim()).join('\n\n') + '\n'
@ -65,7 +71,10 @@ for (const category of fs.readdirSync('messages')) {
'<!-- This file is generated by scripts/process-messages/index.js. Do not edit! -->\n\n' +
Object.entries(messages[category])
.map(([code, { messages, details }]) => {
const chunks = [`### ${code}`, ...messages.map((message) => '```\n' + message + '\n```')];
const chunks = [
`### ${code}`,
...messages.map((message) => '```\n' + message + '\n```')
];
if (details) {
chunks.push(details);
@ -407,3 +416,26 @@ transform('client-errors', 'src/internal/client/errors.js');
transform('server-errors', 'src/internal/server/errors.js');
transform('shared-errors', 'src/internal/shared/errors.js');
transform('shared-warnings', 'src/internal/shared/warnings.js');
}
if (watch) {
let running = false;
let timeout;
fs.watch('messages', { recursive: true }, (type, file) => {
if (running) {
timeout ??= setTimeout(() => {
running = false;
timeout = null;
});
} else {
running = true;
// eslint-disable-next-line no-console
console.log('Regenerating messages...');
run();
}
});
}
run();

@ -0,0 +1,113 @@
/** @import { Action, ActionReturn } from '../action/public' */
/** @import { Attachment } from './public' */
import { noop, render_effect } from 'svelte/internal/client';
import { ATTACHMENT_KEY } from '../constants.js';
import { untrack } from '../index-client.js';
import { teardown } from '../internal/client/reactivity/effects.js';
/**
* Creates an object key that will be recognised as an attachment when the object is spread onto an element,
* as a programmatic alternative to using `{@attach ...}`. This can be useful for library authors, though
* is generally not needed when building an app.
*
* ```svelte
* <script>
* import { createAttachmentKey } from 'svelte/attachments';
*
* const props = {
* class: 'cool',
* onclick: () => alert('clicked'),
* [createAttachmentKey()]: (node) => {
* node.textContent = 'attached!';
* }
* };
* </script>
*
* <button {...props}>click me</button>
* ```
* @since 5.29
*/
export function createAttachmentKey() {
return Symbol(ATTACHMENT_KEY);
}
/**
* Converts an [action](https://svelte.dev/docs/svelte/use) into an [attachment](https://svelte.dev/docs/svelte/@attach) keeping the same behavior.
* It's useful if you want to start using attachments on components but you have actions provided by a library.
*
* Note that the second argument, if provided, must be a function that _returns_ the argument to the
* action function, not the argument itself.
*
* ```svelte
* <!-- with an action -->
* <div use:foo={bar}>...</div>
*
* <!-- with an attachment -->
* <div {@attach fromAction(foo, () => bar)}>...</div>
* ```
* @template {EventTarget} E
* @template {unknown} T
* @overload
* @param {Action<E, T> | ((element: E, arg: T) => void | ActionReturn<T>)} action The action function
* @param {() => T} fn A function that returns the argument for the action
* @returns {Attachment<E>}
*/
/**
* Converts an [action](https://svelte.dev/docs/svelte/use) into an [attachment](https://svelte.dev/docs/svelte/@attach) keeping the same behavior.
* It's useful if you want to start using attachments on components but you have actions provided by a library.
*
* Note that the second argument, if provided, must be a function that _returns_ the argument to the
* action function, not the argument itself.
*
* ```svelte
* <!-- with an action -->
* <div use:foo={bar}>...</div>
*
* <!-- with an attachment -->
* <div {@attach fromAction(foo, () => bar)}>...</div>
* ```
* @template {EventTarget} E
* @overload
* @param {Action<E, void> | ((element: E) => void | ActionReturn<void>)} action The action function
* @returns {Attachment<E>}
*/
/**
* Converts an [action](https://svelte.dev/docs/svelte/use) into an [attachment](https://svelte.dev/docs/svelte/@attach) keeping the same behavior.
* It's useful if you want to start using attachments on components but you have actions provided by a library.
*
* Note that the second argument, if provided, must be a function that _returns_ the argument to the
* action function, not the argument itself.
*
* ```svelte
* <!-- with an action -->
* <div use:foo={bar}>...</div>
*
* <!-- with an attachment -->
* <div {@attach fromAction(foo, () => bar)}>...</div>
* ```
*
* @template {EventTarget} E
* @template {unknown} T
* @param {Action<E, T> | ((element: E, arg: T) => void | ActionReturn<T>)} action The action function
* @param {() => T} fn A function that returns the argument for the action
* @returns {Attachment<E>}
* @since 5.32
*/
export function fromAction(action, fn = /** @type {() => T} */ (noop)) {
return (element) => {
const { update, destroy } = untrack(() => action(element, fn()) ?? {});
if (update) {
var ran = false;
render_effect(() => {
const arg = fn();
if (ran) update(arg);
});
ran = true;
}
if (destroy) {
teardown(destroy);
}
};
}

@ -0,0 +1,12 @@
/**
* An [attachment](https://svelte.dev/docs/svelte/@attach) is a function that runs when an element is mounted
* to the DOM, and optionally returns a function that is called when the element is later removed.
*
* It can be attached to an element with an `{@attach ...}` tag, or by spreading an object containing
* a property created with [`createAttachmentKey`](https://svelte.dev/docs/svelte/svelte-attachments#createAttachmentKey).
*/
export interface Attachment<T extends EventTarget = Element> {
(element: T): void | (() => void);
}
export * from './index.js';

@ -461,6 +461,25 @@ export function snippet_parameter_assignment(node) {
e(node, 'snippet_parameter_assignment', `Cannot reassign or bind to snippet parameter\nhttps://svelte.dev/e/snippet_parameter_assignment`);
}
/**
* `%name%` has already been declared on this class
* @param {null | number | NodeLike} node
* @param {string} name
* @returns {never}
*/
export function state_field_duplicate(node, name) {
e(node, 'state_field_duplicate', `\`${name}\` has already been declared on this class\nhttps://svelte.dev/e/state_field_duplicate`);
}
/**
* Cannot assign to a state field before its declaration
* @param {null | number | NodeLike} node
* @returns {never}
*/
export function state_field_invalid_assignment(node) {
e(node, 'state_field_invalid_assignment', `Cannot assign to a state field before its declaration\nhttps://svelte.dev/e/state_field_invalid_assignment`);
}
/**
* Cannot export state from a module if it is reassigned. Either export a function returning the state value or only mutate the state value's properties
* @param {null | number | NodeLike} node
@ -471,13 +490,13 @@ export function state_invalid_export(node) {
}
/**
* `%rune%(...)` can only be used as a variable declaration initializer or a class field
* `%rune%(...)` can only be used as a variable declaration initializer, a class field declaration, or the first assignment to a class field at the top level of the constructor.
* @param {null | number | NodeLike} node
* @param {string} rune
* @returns {never}
*/
export function state_invalid_placement(node, rune) {
e(node, 'state_invalid_placement', `\`${rune}(...)\` can only be used as a variable declaration initializer or a class field\nhttps://svelte.dev/e/state_invalid_placement`);
e(node, 'state_invalid_placement', `\`${rune}(...)\` can only be used as a variable declaration initializer, a class field declaration, or the first assignment to a class field at the top level of the constructor.\nhttps://svelte.dev/e/state_invalid_placement`);
}
/**

@ -43,6 +43,11 @@ export function compile(source, options) {
instance: parsed.instance && remove_typescript_nodes(parsed.instance),
module: parsed.module && remove_typescript_nodes(parsed.module)
};
if (combined_options.customElementOptions?.extend) {
combined_options.customElementOptions.extend = remove_typescript_nodes(
combined_options.customElementOptions?.extend
);
}
}
const analysis = analyze_component(parsed, source, combined_options);

@ -378,7 +378,8 @@ export function convert(source, ast) {
end: node.end,
expression: node.expression,
parameters: node.parameters,
children: node.body.nodes.map((child) => visit(child))
children: node.body.nodes.map((child) => visit(child)),
typeParams: node.typeParams
};
},
// @ts-expect-error

@ -603,15 +603,15 @@ const instance_script = {
);
// Turn export let into props. It's really really weird because export let { x: foo, z: [bar]} = ..
// means that foo and bar are the props (i.e. the leafs are the prop names), not x and z.
// const tmp = state.scope.generate('tmp');
// const paths = extract_paths(declarator.id);
// const tmp = b.id(state.scope.generate('tmp'));
// const paths = extract_paths(declarator.id, tmp);
// state.props_pre.push(
// b.declaration('const', b.id(tmp), visit(declarator.init!) as Expression)
// b.declaration('const', tmp, visit(declarator.init!) as Expression)
// );
// for (const path of paths) {
// const name = (path.node as Identifier).name;
// const binding = state.scope.get(name)!;
// const value = path.expression!(b.id(tmp));
// const value = path.expression;
// if (binding.kind === 'bindable_prop' || binding.kind === 'rest_prop') {
// state.props.push({
// local: name,

@ -36,7 +36,7 @@ export function parse(source, typescript, is_script) {
ast = parser.parse(source, {
onComment,
sourceType: 'module',
ecmaVersion: 13,
ecmaVersion: 16,
locations: true
});
} finally {
@ -64,7 +64,7 @@ export function parse_expression_at(source, typescript, index) {
const ast = parser.parseExpressionAt(source, index, {
onComment,
sourceType: 'module',
ecmaVersion: 13,
ecmaVersion: 16,
locations: true
});

@ -1,7 +1,7 @@
/** @import { Location } from 'locate-character' */
/** @import { Pattern } from 'estree' */
/** @import { Parser } from '../index.js' */
import { is_bracket_open, is_bracket_close, get_bracket_close } from '../utils/bracket.js';
import { match_bracket } from '../utils/bracket.js';
import { parse_expression_at } from '../acorn.js';
import { regex_not_newline_characters } from '../../patterns.js';
import * as e from '../../../errors.js';
@ -33,7 +33,9 @@ export default function read_pattern(parser) {
};
}
if (!is_bracket_open(parser.template[i])) {
const char = parser.template[i];
if (char !== '{' && char !== '[') {
e.expected_pattern(i);
}
@ -71,75 +73,6 @@ export default function read_pattern(parser) {
}
}
/**
* @param {Parser} parser
* @param {number} start
*/
function match_bracket(parser, start) {
const bracket_stack = [];
let i = start;
while (i < parser.template.length) {
let char = parser.template[i++];
if (char === "'" || char === '"' || char === '`') {
i = match_quote(parser, i, char);
continue;
}
if (is_bracket_open(char)) {
bracket_stack.push(char);
} else if (is_bracket_close(char)) {
const popped = /** @type {string} */ (bracket_stack.pop());
const expected = /** @type {string} */ (get_bracket_close(popped));
if (char !== expected) {
e.expected_token(i - 1, expected);
}
if (bracket_stack.length === 0) {
return i;
}
}
}
e.unexpected_eof(parser.template.length);
}
/**
* @param {Parser} parser
* @param {number} start
* @param {string} quote
*/
function match_quote(parser, start, quote) {
let is_escaped = false;
let i = start;
while (i < parser.template.length) {
const char = parser.template[i++];
if (is_escaped) {
is_escaped = false;
continue;
}
if (char === quote) {
return i;
}
if (char === '\\') {
is_escaped = true;
}
if (quote === '`' && char === '$' && parser.template[i] === '{') {
i = match_bracket(parser, i);
}
}
e.unterminated_string_constant(start);
}
/**
* @param {Parser} parser
* @returns {any}

@ -16,7 +16,7 @@ const ALLOWED_ATTRIBUTES = ['context', 'generics', 'lang', 'module'];
/**
* @param {Parser} parser
* @param {number} start
* @param {Array<AST.Attribute | AST.SpreadAttribute | AST.Directive>} attributes
* @param {Array<AST.Attribute | AST.SpreadAttribute | AST.Directive | AST.AttachTag>} attributes
* @returns {AST.Script}
*/
export function read_script(parser, start, attributes) {

@ -12,13 +12,14 @@ const REGEX_NTH_OF =
const REGEX_WHITESPACE_OR_COLON = /[\s:]/;
const REGEX_LEADING_HYPHEN_OR_DIGIT = /-?\d/;
const REGEX_VALID_IDENTIFIER_CHAR = /[a-zA-Z0-9_-]/;
const REGEX_UNICODE_SEQUENCE = /^\\[0-9a-fA-F]{1,6}(\r\n|\s)?/;
const REGEX_COMMENT_CLOSE = /\*\//;
const REGEX_HTML_COMMENT_CLOSE = /-->/;
/**
* @param {Parser} parser
* @param {number} start
* @param {Array<AST.Attribute | AST.SpreadAttribute | AST.Directive>} attributes
* @param {Array<AST.Attribute | AST.SpreadAttribute | AST.Directive | AST.AttachTag>} attributes
* @returns {AST.CSS.StyleSheet}
*/
export default function read_style(parser, start, attributes) {
@ -580,25 +581,26 @@ function read_identifier(parser) {
e.css_expected_identifier(start);
}
let escaped = false;
while (parser.index < parser.template.length) {
const char = parser.template[parser.index];
if (escaped) {
identifier += '\\' + char;
escaped = false;
} else if (char === '\\') {
escaped = true;
if (char === '\\') {
const sequence = parser.match_regex(REGEX_UNICODE_SEQUENCE);
if (sequence) {
identifier += String.fromCodePoint(parseInt(sequence.slice(1), 16));
parser.index += sequence.length;
} else {
identifier += '\\' + parser.template[parser.index + 1];
parser.index += 2;
}
} else if (
/** @type {number} */ (char.codePointAt(0)) >= 160 ||
REGEX_VALID_IDENTIFIER_CHAR.test(char)
) {
identifier += char;
parser.index++;
} else {
break;
}
parser.index++;
}
if (identifier === '') {

@ -115,6 +115,19 @@ const visitors = {
TSDeclareFunction() {
return b.empty;
},
ClassBody(node, context) {
const body = [];
for (const _child of node.body) {
const child = context.visit(_child);
if (child.type !== 'PropertyDefinition' || !child.declare) {
body.push(child);
}
}
return {
...node,
body
};
},
ClassDeclaration(node, context) {
if (node.declare) {
return b.empty;

@ -93,7 +93,16 @@ export default function element(parser) {
}
}
if (parent.type !== 'RegularElement' && !parser.loose) {
if (parent.type === 'RegularElement') {
if (!parser.last_auto_closed_tag || parser.last_auto_closed_tag.tag !== name) {
const end = parent.fragment.nodes[0]?.start ?? start;
w.element_implicitly_closed(
{ start: parent.start, end },
`</${name}>`,
`</${parent.name}>`
);
}
} else if (!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 {
@ -186,6 +195,8 @@ export default function element(parser) {
parser.allow_whitespace();
if (parent.type === 'RegularElement' && closing_tag_omitted(parent.name, name)) {
const end = parent.fragment.nodes[0]?.start ?? start;
w.element_implicitly_closed({ start: parent.start, end }, `<${name}>`, `</${parent.name}>`);
parent.end = start;
parser.pop();
parser.last_auto_closed_tag = {
@ -480,7 +491,7 @@ function read_static_attribute(parser) {
/**
* @param {Parser} parser
* @returns {AST.Attribute | AST.SpreadAttribute | AST.Directive | null}
* @returns {AST.Attribute | AST.SpreadAttribute | AST.Directive | AST.AttachTag | null}
*/
function read_attribute(parser) {
const start = parser.index;
@ -488,6 +499,27 @@ function read_attribute(parser) {
if (parser.eat('{')) {
parser.allow_whitespace();
if (parser.eat('@attach')) {
parser.require_whitespace();
const expression = read_expression(parser);
parser.allow_whitespace();
parser.eat('}', true);
/** @type {AST.AttachTag} */
const attachment = {
type: 'AttachTag',
start,
end: parser.index,
expression,
metadata: {
expression: create_expression_metadata()
}
};
return attachment;
}
if (parser.eat('...')) {
const expression = read_expression(parser);

@ -8,9 +8,12 @@ import { parse_expression_at } from '../acorn.js';
import read_pattern from '../read/context.js';
import read_expression, { get_loose_identifier } from '../read/expression.js';
import { create_fragment } from '../utils/create.js';
import { match_bracket } from '../utils/bracket.js';
const regex_whitespace_with_closing_curly_brace = /^\s*}/;
const pointy_bois = { '<': '>' };
/** @param {Parser} parser */
export default function tag(parser) {
const start = parser.index;
@ -351,6 +354,22 @@ function open(parser) {
const params_start = parser.index;
// snippets could have a generic signature, e.g. `#snippet foo<T>(...)`
/** @type {string | undefined} */
let type_params;
// if we match a generic opening
if (parser.ts && parser.match('<')) {
const start = parser.index;
const end = match_bracket(parser, start, pointy_bois);
type_params = parser.template.slice(start + 1, end - 1);
parser.index = end;
}
parser.allow_whitespace();
const matched = parser.eat('(', true, false);
if (matched) {
@ -388,6 +407,7 @@ function open(parser) {
end: name_end,
name
},
typeParams: type_params,
parameters: function_expression.params,
body: create_fragment(),
metadata: {

@ -1,34 +1,5 @@
const SQUARE_BRACKET_OPEN = '[';
const SQUARE_BRACKET_CLOSE = ']';
const CURLY_BRACKET_OPEN = '{';
const CURLY_BRACKET_CLOSE = '}';
const PARENTHESES_OPEN = '(';
const PARENTHESES_CLOSE = ')';
/** @param {string} char */
export function is_bracket_open(char) {
return char === SQUARE_BRACKET_OPEN || char === CURLY_BRACKET_OPEN;
}
/** @param {string} char */
export function is_bracket_close(char) {
return char === SQUARE_BRACKET_CLOSE || char === CURLY_BRACKET_CLOSE;
}
/** @param {string} open */
export function get_bracket_close(open) {
if (open === SQUARE_BRACKET_OPEN) {
return SQUARE_BRACKET_CLOSE;
}
if (open === CURLY_BRACKET_OPEN) {
return CURLY_BRACKET_CLOSE;
}
if (open === PARENTHESES_OPEN) {
return PARENTHESES_CLOSE;
}
}
/** @import { Parser } from '../index.js' */
import * as e from '../../../errors.js';
/**
* @param {number} num
@ -121,7 +92,7 @@ function count_leading_backslashes(string, search_start_index) {
* @returns {number | undefined} The index of the closing bracket, or undefined if not found.
*/
export function find_matching_bracket(template, index, open) {
const close = get_bracket_close(open);
const close = default_brackets[open];
let brackets = 1;
let i = index;
while (brackets > 0 && i < template.length) {
@ -162,3 +133,81 @@ export function find_matching_bracket(template, index, open) {
}
return undefined;
}
/** @type {Record<string, string>} */
const default_brackets = {
'{': '}',
'(': ')',
'[': ']'
};
/**
* @param {Parser} parser
* @param {number} start
* @param {Record<string, string>} brackets
*/
export function match_bracket(parser, start, brackets = default_brackets) {
const close = Object.values(brackets);
const bracket_stack = [];
let i = start;
while (i < parser.template.length) {
let char = parser.template[i++];
if (char === "'" || char === '"' || char === '`') {
i = match_quote(parser, i, char);
continue;
}
if (char in brackets) {
bracket_stack.push(char);
} else if (close.includes(char)) {
const popped = /** @type {string} */ (bracket_stack.pop());
const expected = /** @type {string} */ (brackets[popped]);
if (char !== expected) {
e.expected_token(i - 1, expected);
}
if (bracket_stack.length === 0) {
return i;
}
}
}
e.unexpected_eof(parser.template.length);
}
/**
* @param {Parser} parser
* @param {number} start
* @param {string} quote
*/
function match_quote(parser, start, quote) {
let is_escaped = false;
let i = start;
while (i < parser.template.length) {
const char = parser.template[i++];
if (is_escaped) {
is_escaped = false;
continue;
}
if (char === quote) {
return i;
}
if (char === '\\') {
is_escaped = true;
}
if (quote === '`' && char === '$' && parser.template[i] === '{') {
i = match_bracket(parser, i);
}
}
e.unterminated_string_constant(start);
}

@ -18,6 +18,7 @@ import { extract_svelte_ignore } from '../../utils/extract_svelte_ignore.js';
import { ignore_map, ignore_stack, pop_ignore, push_ignore } from '../../state.js';
import { ArrowFunctionExpression } from './visitors/ArrowFunctionExpression.js';
import { AssignmentExpression } from './visitors/AssignmentExpression.js';
import { AttachTag } from './visitors/AttachTag.js';
import { Attribute } from './visitors/Attribute.js';
import { AwaitBlock } from './visitors/AwaitBlock.js';
import { BindDirective } from './visitors/BindDirective.js';
@ -43,9 +44,11 @@ import { ImportDeclaration } from './visitors/ImportDeclaration.js';
import { KeyBlock } from './visitors/KeyBlock.js';
import { LabeledStatement } from './visitors/LabeledStatement.js';
import { LetDirective } from './visitors/LetDirective.js';
import { Literal } from './visitors/Literal.js';
import { MemberExpression } from './visitors/MemberExpression.js';
import { NewExpression } from './visitors/NewExpression.js';
import { OnDirective } from './visitors/OnDirective.js';
import { PropertyDefinition } from './visitors/PropertyDefinition.js';
import { RegularElement } from './visitors/RegularElement.js';
import { RenderTag } from './visitors/RenderTag.js';
import { SlotElement } from './visitors/SlotElement.js';
@ -63,6 +66,7 @@ import { SvelteSelf } from './visitors/SvelteSelf.js';
import { SvelteWindow } from './visitors/SvelteWindow.js';
import { SvelteBoundary } from './visitors/SvelteBoundary.js';
import { TaggedTemplateExpression } from './visitors/TaggedTemplateExpression.js';
import { TemplateElement } from './visitors/TemplateElement.js';
import { Text } from './visitors/Text.js';
import { TitleElement } from './visitors/TitleElement.js';
import { TransitionDirective } from './visitors/TransitionDirective.js';
@ -131,6 +135,7 @@ const visitors = {
},
ArrowFunctionExpression,
AssignmentExpression,
AttachTag,
Attribute,
AwaitBlock,
BindDirective,
@ -156,9 +161,11 @@ const visitors = {
KeyBlock,
LabeledStatement,
LetDirective,
Literal,
MemberExpression,
NewExpression,
OnDirective,
PropertyDefinition,
RegularElement,
RenderTag,
SlotElement,
@ -176,6 +183,7 @@ const visitors = {
SvelteWindow,
SvelteBoundary,
TaggedTemplateExpression,
TemplateElement,
Text,
TransitionDirective,
TitleElement,
@ -250,7 +258,8 @@ export function analyze_module(ast, options) {
accessors: false,
runes: true,
immutable: true,
tracing: false
tracing: false,
classes: new Map()
};
walk(
@ -259,7 +268,7 @@ export function analyze_module(ast, options) {
scope,
scopes,
analysis: /** @type {ComponentAnalysis} */ (analysis),
derived_state: [],
state_fields: new Map(),
// TODO the following are not needed for modules, but we have to pass them in order to avoid type error,
// and reducing the type would result in a lot of tedious type casts elsewhere - find a good solution one day
ast_type: /** @type {any} */ (null),
@ -423,6 +432,7 @@ export function analyze_component(root, source, options) {
elements: [],
runes,
tracing: false,
classes: new Map(),
immutable: runes || options.immutable,
exports: [],
uses_props: false,
@ -618,7 +628,7 @@ export function analyze_component(root, source, options) {
has_props_rune: false,
component_slots: new Set(),
expression: null,
derived_state: [],
state_fields: new Map(),
function_depth: scope.function_depth,
reactive_statement: null
};
@ -685,7 +695,7 @@ export function analyze_component(root, source, options) {
reactive_statement: null,
component_slots: new Set(),
expression: null,
derived_state: [],
state_fields: new Map(),
function_depth: scope.function_depth
};

@ -1,6 +1,6 @@
import type { Scope } from '../scope.js';
import type { ComponentAnalysis, ReactiveStatement } from '../types.js';
import type { AST, ExpressionMetadata, ValidatedCompileOptions } from '#compiler';
import type { AST, ExpressionMetadata, StateField, ValidatedCompileOptions } from '#compiler';
export interface AnalysisState {
scope: Scope;
@ -18,7 +18,10 @@ export interface AnalysisState {
component_slots: Set<string>;
/** Information about the current expression/directive/block value */
expression: ExpressionMetadata | null;
derived_state: { name: string; private: boolean }[];
/** Used to analyze class state */
state_fields: Map<string, StateField>;
function_depth: number;
// legacy stuff

@ -8,7 +8,7 @@ import { validate_assignment } from './shared/utils.js';
* @param {Context} context
*/
export function AssignmentExpression(node, context) {
validate_assignment(node, node.left, context.state);
validate_assignment(node, node.left, context);
if (context.state.reactive_statement) {
const id = node.left.type === 'MemberExpression' ? object(node.left) : node.left;

@ -0,0 +1,13 @@
/** @import { AST } from '#compiler' */
/** @import { Context } from '../types' */
import { mark_subtree_dynamic } from './shared/fragment.js';
/**
* @param {AST.AttachTag} node
* @param {Context} context
*/
export function AttachTag(node, context) {
mark_subtree_dynamic(context.path);
context.next({ ...context.state, expression: node.metadata.expression });
}

@ -211,7 +211,7 @@ function get_delegated_event(event_name, handler, context) {
if (
binding !== null &&
// Bail out if the the binding is a rest param
// Bail out if the binding is a rest param
(binding.declaration_kind === 'rest_param' ||
// Bail out if we reference anything from the EachBlock (for now) that mutates in non-runes mode,
(((!context.state.analysis.runes && binding.kind === 'each') ||

@ -158,7 +158,7 @@ export function BindDirective(node, context) {
return;
}
validate_assignment(node, node.expression, context.state);
validate_assignment(node, node.expression, context);
const assignee = node.expression;
const left = object(assignee);

@ -114,12 +114,13 @@ export function CallExpression(node, context) {
case '$state':
case '$state.raw':
case '$derived':
case '$derived.by':
if (
(parent.type !== 'VariableDeclarator' ||
get_parent(context.path, -3).type === 'ConstTag') &&
!(parent.type === 'PropertyDefinition' && !parent.static && !parent.computed)
) {
case '$derived.by': {
const valid =
is_variable_declaration(parent, context) ||
is_class_property_definition(parent) ||
is_class_property_assignment_at_constructor_root(parent, context);
if (!valid) {
e.state_invalid_placement(node, rune);
}
@ -130,6 +131,7 @@ export function CallExpression(node, context) {
}
break;
}
case '$effect':
case '$effect.pre':
@ -270,3 +272,40 @@ function get_function_label(nodes) {
return parent.id.name;
}
}
/**
* @param {AST.SvelteNode} parent
* @param {Context} context
*/
function is_variable_declaration(parent, context) {
return parent.type === 'VariableDeclarator' && get_parent(context.path, -3).type !== 'ConstTag';
}
/**
* @param {AST.SvelteNode} parent
*/
function is_class_property_definition(parent) {
return parent.type === 'PropertyDefinition' && !parent.static && !parent.computed;
}
/**
* @param {AST.SvelteNode} node
* @param {Context} context
*/
function is_class_property_assignment_at_constructor_root(node, context) {
if (
node.type === 'AssignmentExpression' &&
node.operator === '=' &&
node.left.type === 'MemberExpression' &&
node.left.object.type === 'ThisExpression' &&
((node.left.property.type === 'Identifier' && !node.left.computed) ||
node.left.property.type === 'PrivateIdentifier' ||
node.left.property.type === 'Literal')
) {
// MethodDefinition (-5) -> FunctionExpression (-4) -> BlockStatement (-3) -> ExpressionStatement (-2) -> AssignmentExpression (-1)
const parent = get_parent(context.path, -5);
return parent?.type === 'MethodDefinition' && parent.kind === 'constructor';
}
return false;
}

@ -1,30 +1,107 @@
/** @import { ClassBody } from 'estree' */
/** @import { AssignmentExpression, CallExpression, ClassBody, PropertyDefinition, Expression, PrivateIdentifier, MethodDefinition } from 'estree' */
/** @import { StateField } from '#compiler' */
/** @import { Context } from '../types' */
import * as b from '#compiler/builders';
import { get_rune } from '../../scope.js';
import * as e from '../../../errors.js';
import { is_state_creation_rune } from '../../../../utils.js';
import { get_name } from '../../nodes.js';
import { regex_invalid_identifier_chars } from '../../patterns.js';
/**
* @param {ClassBody} node
* @param {Context} context
*/
export function ClassBody(node, context) {
/** @type {{name: string, private: boolean}[]} */
const derived_state = [];
if (!context.state.analysis.runes) {
context.next();
return;
}
/** @type {string[]} */
const private_ids = [];
for (const definition of node.body) {
for (const prop of node.body) {
if (
definition.type === 'PropertyDefinition' &&
(definition.key.type === 'PrivateIdentifier' || definition.key.type === 'Identifier') &&
definition.value?.type === 'CallExpression'
(prop.type === 'MethodDefinition' || prop.type === 'PropertyDefinition') &&
prop.key.type === 'PrivateIdentifier'
) {
const rune = get_rune(definition.value, context.state.scope);
if (rune === '$derived' || rune === '$derived.by') {
derived_state.push({
name: definition.key.name,
private: definition.key.type === 'PrivateIdentifier'
private_ids.push(prop.key.name);
}
}
/** @type {Map<string, StateField>} */
const state_fields = new Map();
context.state.analysis.classes.set(node, state_fields);
/** @type {MethodDefinition | null} */
let constructor = null;
/**
* @param {PropertyDefinition | AssignmentExpression} node
* @param {Expression | PrivateIdentifier} key
* @param {Expression | null | undefined} value
*/
function handle(node, key, value) {
const name = get_name(key);
if (name === null) return;
const rune = get_rune(value, context.state.scope);
if (rune && is_state_creation_rune(rune)) {
if (state_fields.has(name)) {
e.state_field_duplicate(node, name);
}
state_fields.set(name, {
node,
type: rune,
// @ts-expect-error for public state this is filled out in a moment
key: key.type === 'PrivateIdentifier' ? key : null,
value: /** @type {CallExpression} */ (value)
});
}
}
for (const child of node.body) {
if (child.type === 'PropertyDefinition' && !child.computed && !child.static) {
handle(child, child.key, child.value);
}
if (child.type === 'MethodDefinition' && child.kind === 'constructor') {
constructor = child;
}
}
if (constructor) {
for (const statement of constructor.value.body.body) {
if (statement.type !== 'ExpressionStatement') continue;
if (statement.expression.type !== 'AssignmentExpression') continue;
const { left, right } = statement.expression;
if (left.type !== 'MemberExpression') continue;
if (left.object.type !== 'ThisExpression') continue;
if (left.computed && left.property.type !== 'Literal') continue;
handle(statement.expression, left.property, right);
}
}
for (const [name, field] of state_fields) {
if (name[0] === '#') {
continue;
}
let deconflicted = name.replace(regex_invalid_identifier_chars, '_');
while (private_ids.includes(deconflicted)) {
deconflicted = '_' + deconflicted;
}
private_ids.push(deconflicted);
field.key = b.private_id(deconflicted);
}
context.next({ ...context.state, derived_state });
context.next({ ...context.state, state_fields });
}

@ -1,7 +1,8 @@
/** @import { AST } from '#compiler' */
/** @import { AST, Binding } from '#compiler' */
/** @import { Context } from '../types' */
/** @import { Scope } from '../../scope' */
import * as e from '../../../errors.js';
import { extract_identifiers } from '../../../utils/ast.js';
import { mark_subtree_dynamic } from './shared/fragment.js';
import { validate_block_not_empty, validate_opening_tag } from './shared/utils.js';
@ -38,5 +39,52 @@ export function EachBlock(node, context) {
if (node.key) context.visit(node.key);
if (node.fallback) context.visit(node.fallback);
if (!context.state.analysis.runes) {
let mutated =
!!node.context &&
extract_identifiers(node.context).some((id) => {
const binding = context.state.scope.get(id.name);
return !!binding?.mutated;
});
// collect transitive dependencies...
for (const binding of node.metadata.expression.dependencies) {
collect_transitive_dependencies(binding, node.metadata.transitive_deps);
}
// ...and ensure they are marked as state, so they can be turned
// into mutable sources and invalidated
if (mutated) {
for (const binding of node.metadata.transitive_deps) {
if (
binding.kind === 'normal' &&
(binding.declaration_kind === 'const' ||
binding.declaration_kind === 'let' ||
binding.declaration_kind === 'var')
) {
binding.kind = 'state';
}
}
}
}
mark_subtree_dynamic(context.path);
}
/**
* @param {Binding} binding
* @param {Set<Binding>} bindings
* @returns {void}
*/
function collect_transitive_dependencies(binding, bindings) {
if (bindings.has(binding)) {
return;
}
bindings.add(binding);
if (binding.kind === 'legacy_reactive') {
for (const dep of binding.legacy_dependencies) {
collect_transitive_dependencies(dep, bindings);
}
}
}

@ -39,7 +39,7 @@ export function Identifier(node, context) {
if (
is_rune(node.name) &&
context.state.scope.get(node.name) === null &&
context.state.scope.get(node.name.slice(1)) === null
context.state.scope.get(node.name.slice(1))?.kind !== 'store_sub'
) {
/** @type {Expression} */
let current = node;
@ -90,7 +90,10 @@ export function Identifier(node, context) {
if (binding) {
if (context.state.expression) {
context.state.expression.dependencies.add(binding);
context.state.expression.has_state ||= binding.kind !== 'normal';
context.state.expression.has_state ||=
binding.kind !== 'static' &&
!binding.is_function() &&
!context.state.scope.evaluate(node).is_known;
}
if (

@ -0,0 +1,14 @@
/** @import { Literal } from 'estree' */
import * as w from '../../../warnings.js';
import { regex_bidirectional_control_characters } from '../../patterns.js';
/**
* @param {Literal} node
*/
export function Literal(node) {
if (typeof node.value === 'string') {
if (regex_bidirectional_control_characters.test(node.value)) {
w.bidirectional_control_characters(node);
}
}
}

@ -0,0 +1,21 @@
/** @import { PropertyDefinition } from 'estree' */
/** @import { Context } from '../types' */
import * as e from '../../../errors.js';
import { get_name } from '../../nodes.js';
/**
* @param {PropertyDefinition} node
* @param {Context} context
*/
export function PropertyDefinition(node, context) {
const name = get_name(node.key);
const field = name && context.state.state_fields.get(name);
if (field && node !== field.node && node.value) {
if (/** @type {number} */ (node.start) < /** @type {number} */ (field.node.start)) {
e.state_field_invalid_assignment(node);
}
}
context.next();
}

@ -0,0 +1,12 @@
/** @import { TemplateElement } from 'estree' */
import * as w from '../../../warnings.js';
import { regex_bidirectional_control_characters } from '../../patterns.js';
/**
* @param {TemplateElement} node
*/
export function TemplateElement(node) {
if (regex_bidirectional_control_characters.test(node.value.cooked ?? '')) {
w.bidirectional_control_characters(node);
}
}

@ -1,20 +1,52 @@
/** @import { AST } from '#compiler' */
/** @import { Context } from '../types' */
import { is_tag_valid_with_parent } from '../../../../html-tree-validation.js';
import { regex_not_whitespace } from '../../patterns.js';
import { regex_bidirectional_control_characters, regex_not_whitespace } from '../../patterns.js';
import * as e from '../../../errors.js';
import * as w from '../../../warnings.js';
import { extract_svelte_ignore } from '../../../utils/extract_svelte_ignore.js';
/**
* @param {AST.Text} node
* @param {Context} context
*/
export function Text(node, context) {
const in_template = context.path.at(-1)?.type === 'Fragment';
const parent = /** @type {AST.SvelteNode} */ (context.path.at(-1));
if (in_template && context.state.parent_element && regex_not_whitespace.test(node.data)) {
if (
parent.type === 'Fragment' &&
context.state.parent_element &&
regex_not_whitespace.test(node.data)
) {
const message = is_tag_valid_with_parent('#text', context.state.parent_element);
if (message) {
e.node_invalid_placement(node, message);
}
}
regex_bidirectional_control_characters.lastIndex = 0;
for (const match of node.data.matchAll(regex_bidirectional_control_characters)) {
let is_ignored = false;
// if we have a svelte-ignore comment earlier in the text, bail
// (otherwise we can only use svelte-ignore on parent elements/blocks)
if (parent.type === 'Fragment') {
for (const child of parent.nodes) {
if (child === node) break;
if (child.type === 'Comment') {
is_ignored ||= extract_svelte_ignore(
child.start + 4,
child.data,
context.state.analysis.runes
).includes('bidirectional_control_characters');
}
}
}
if (!is_ignored) {
let start = match.index + node.start;
w.bidirectional_control_characters({ start, end: start + match[0].length });
}
}
}

@ -8,7 +8,7 @@ import { validate_assignment } from './shared/utils.js';
* @param {Context} context
*/
export function UpdateExpression(node, context) {
validate_assignment(node, node.argument, context.state);
validate_assignment(node, node.argument, context);
if (context.state.reactive_statement) {
const id = node.argument.type === 'MemberExpression' ? object(node.argument) : node.argument;

@ -4,8 +4,10 @@
import { get_rune } from '../../scope.js';
import { ensure_no_module_import_conflict, validate_identifier_name } from './shared/utils.js';
import * as e from '../../../errors.js';
import * as w from '../../../warnings.js';
import { extract_paths } from '../../../utils/ast.js';
import { equal } from '../../../utils/assert.js';
import * as b from '#compiler/builders';
/**
* @param {VariableDeclarator} node
@ -17,7 +19,7 @@ export function VariableDeclarator(node, context) {
if (context.state.analysis.runes) {
const init = node.init;
const rune = get_rune(init, context.state.scope);
const paths = extract_paths(node.id);
const { paths } = extract_paths(node.id, b.id('dummy'));
for (const path of paths) {
validate_identifier_name(context.state.scope.get(/** @type {Identifier} */ (path.node).name));
@ -52,6 +54,19 @@ export function VariableDeclarator(node, context) {
e.props_invalid_identifier(node);
}
if (
context.state.analysis.custom_element &&
context.state.options.customElementOptions?.props == null
) {
let warn_on;
if (
node.id.type === 'Identifier' ||
(warn_on = node.id.properties.find((p) => p.type === 'RestElement')) != null
) {
w.custom_element_props_identifier(warn_on ?? node.id);
}
}
context.state.analysis.needs_props = true;
if (node.id.type === 'Identifier') {

@ -1,3 +1,4 @@
/** @import { Expression } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { AnalysisState, Context } from '../../types' */
import * as e from '../../../../errors.js';
@ -74,7 +75,8 @@ export function visit_component(node, context) {
attribute.type !== 'SpreadAttribute' &&
attribute.type !== 'LetDirective' &&
attribute.type !== 'OnDirective' &&
attribute.type !== 'BindDirective'
attribute.type !== 'BindDirective' &&
attribute.type !== 'AttachTag'
) {
e.component_invalid_directive(attribute);
}
@ -91,15 +93,10 @@ export function visit_component(node, context) {
validate_attribute(attribute, node);
if (is_expression_attribute(attribute)) {
const expression = get_attribute_expression(attribute);
if (expression.type === 'SequenceExpression') {
let i = /** @type {number} */ (expression.start);
while (--i > 0) {
const char = context.state.analysis.source[i];
if (char === '(') break; // parenthesized sequence expressions are ok
if (char === '{') e.attribute_invalid_sequence_expression(expression);
}
}
disallow_unparenthesized_sequences(
get_attribute_expression(attribute),
context.state.analysis.source
);
}
}
@ -113,6 +110,10 @@ export function visit_component(node, context) {
if (attribute.type === 'BindDirective' && attribute.name !== 'this') {
context.state.analysis.uses_component_bindings = true;
}
if (attribute.type === 'AttachTag') {
disallow_unparenthesized_sequences(attribute.expression, context.state.analysis.source);
}
}
// If the component has a slot attribute — `<Foo slot="whatever" .../>` —
@ -158,3 +159,18 @@ export function visit_component(node, context) {
context.visit({ ...node.fragment, nodes: nodes[slot_name] }, state);
}
}
/**
* @param {Expression} expression
* @param {string} source
*/
function disallow_unparenthesized_sequences(expression, source) {
if (expression.type === 'SequenceExpression') {
let i = /** @type {number} */ (expression.start);
while (--i > 0) {
const char = source[i];
if (char === '(') break; // parenthesized sequence expressions are ok
if (char === '{') e.attribute_invalid_sequence_expression(expression);
}
}
}

@ -4,24 +4,25 @@
/** @import { Scope } from '../../../scope' */
/** @import { NodeLike } from '../../../../errors.js' */
import * as e from '../../../../errors.js';
import { extract_identifiers } from '../../../../utils/ast.js';
import { extract_identifiers, get_parent } from '../../../../utils/ast.js';
import * as w from '../../../../warnings.js';
import * as b from '#compiler/builders';
import { get_rune } from '../../../scope.js';
import { get_name } from '../../../nodes.js';
/**
* @param {AssignmentExpression | UpdateExpression | AST.BindDirective} node
* @param {Pattern | Expression} argument
* @param {AnalysisState} state
* @param {Context} context
*/
export function validate_assignment(node, argument, state) {
validate_no_const_assignment(node, argument, state.scope, node.type === 'BindDirective');
export function validate_assignment(node, argument, context) {
validate_no_const_assignment(node, argument, context.state.scope, node.type === 'BindDirective');
if (argument.type === 'Identifier') {
const binding = state.scope.get(argument.name);
const binding = context.state.scope.get(argument.name);
if (state.analysis.runes) {
if (binding?.node === state.analysis.props_id) {
if (context.state.analysis.runes) {
if (binding?.node === context.state.analysis.props_id) {
e.constant_assignment(node, '$props.id()');
}
@ -34,6 +35,41 @@ export function validate_assignment(node, argument, state) {
e.snippet_parameter_assignment(node);
}
}
if (argument.type === 'MemberExpression' && argument.object.type === 'ThisExpression') {
const name =
argument.computed && argument.property.type !== 'Literal'
? null
: get_name(argument.property);
const field = name !== null && context.state.state_fields?.get(name);
// check we're not assigning to a state field before its declaration in the constructor
if (field && field.node.type === 'AssignmentExpression' && node !== field.node) {
let i = context.path.length;
while (i--) {
const parent = context.path[i];
if (
parent.type === 'FunctionDeclaration' ||
parent.type === 'FunctionExpression' ||
parent.type === 'ArrowFunctionExpression'
) {
const grandparent = get_parent(context.path, i - 1);
if (
grandparent.type === 'MethodDefinition' &&
grandparent.kind === 'constructor' &&
/** @type {number} */ (node.start) < /** @type {number} */ (field.node.start)
) {
e.state_field_invalid_assignment(node);
}
break;
}
}
}
}
}
/**

@ -56,6 +56,7 @@ import { TitleElement } from './visitors/TitleElement.js';
import { TransitionDirective } from './visitors/TransitionDirective.js';
import { UpdateExpression } from './visitors/UpdateExpression.js';
import { UseDirective } from './visitors/UseDirective.js';
import { AttachTag } from './visitors/AttachTag.js';
import { VariableDeclaration } from './visitors/VariableDeclaration.js';
/** @type {Visitors} */
@ -131,6 +132,7 @@ const visitors = {
TransitionDirective,
UpdateExpression,
UseDirective,
AttachTag,
VariableDeclaration
};
@ -152,17 +154,12 @@ export function client_component(analysis, options) {
legacy_reactive_imports: [],
legacy_reactive_statements: new Map(),
metadata: {
context: {
template_needs_import_node: false,
template_contains_script_tag: false
},
namespace: options.namespace,
bound_contenteditable: false
},
events: new Set(),
preserve_whitespace: options.preserveWhitespace,
public_state: new Map(),
private_state: new Map(),
state_fields: new Map(),
transform: {},
in_constructor: false,
instance_level_snippets: [],
@ -173,8 +170,7 @@ export function client_component(analysis, options) {
update: /** @type {any} */ (null),
expressions: /** @type {any} */ (null),
after_update: /** @type {any} */ (null),
template: /** @type {any} */ (null),
locations: /** @type {any} */ (null)
template: /** @type {any} */ (null)
};
const module = /** @type {ESTree.Program} */ (
@ -669,8 +665,7 @@ export function client_module(analysis, options) {
options,
scope: analysis.module.scope,
scopes: analysis.module.scopes,
public_state: new Map(),
private_state: new Map(),
state_fields: new Map(),
transform: {},
in_constructor: false
};

@ -0,0 +1,18 @@
const svg_attributes =
'accent-height accumulate additive alignment-baseline allowReorder alphabetic amplitude arabic-form ascent attributeName attributeType autoReverse azimuth baseFrequency baseline-shift baseProfile bbox begin bias by calcMode cap-height class clip clipPathUnits clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering contentScriptType contentStyleType cursor cx cy d decelerate descent diffuseConstant direction display divisor dominant-baseline dur dx dy edgeMode elevation enable-background end exponent externalResourcesRequired fill fill-opacity fill-rule filter filterRes filterUnits flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight format from fr fx fy g1 g2 glyph-name glyph-orientation-horizontal glyph-orientation-vertical glyphRef gradientTransform gradientUnits hanging height href horiz-adv-x horiz-origin-x id ideographic image-rendering in in2 intercept k k1 k2 k3 k4 kernelMatrix kernelUnitLength kerning keyPoints keySplines keyTimes lang lengthAdjust letter-spacing lighting-color limitingConeAngle local marker-end marker-mid marker-start markerHeight markerUnits markerWidth mask maskContentUnits maskUnits mathematical max media method min mode name numOctaves offset onabort onactivate onbegin onclick onend onerror onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup onrepeat onresize onscroll onunload opacity operator order orient orientation origin overflow overline-position overline-thickness panose-1 paint-order pathLength patternContentUnits patternTransform patternUnits pointer-events points pointsAtX pointsAtY pointsAtZ preserveAlpha preserveAspectRatio primitiveUnits r radius refX refY rendering-intent repeatCount repeatDur requiredExtensions requiredFeatures restart result rotate rx ry scale seed shape-rendering slope spacing specularConstant specularExponent speed spreadMethod startOffset stdDeviation stemh stemv stitchTiles stop-color stop-opacity strikethrough-position strikethrough-thickness string stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style surfaceScale systemLanguage tabindex tableValues target targetX targetY text-anchor text-decoration text-rendering textLength to transform type u1 u2 underline-position underline-thickness unicode unicode-bidi unicode-range units-per-em v-alphabetic v-hanging v-ideographic v-mathematical values version vert-adv-y vert-origin-x vert-origin-y viewBox viewTarget visibility width widths word-spacing writing-mode x x-height x1 x2 xChannelSelector xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space y y1 y2 yChannelSelector z zoomAndPan'.split(
' '
);
const svg_attribute_lookup = new Map();
svg_attributes.forEach((name) => {
svg_attribute_lookup.set(name.toLowerCase(), name);
});
/**
* @param {string} name
*/
export default function fix_attribute_casing(name) {
name = name.toLowerCase();
return svg_attribute_lookup.get(name) || name;
}

@ -0,0 +1,68 @@
/** @import { Location } from 'locate-character' */
/** @import { Namespace } from '#compiler' */
/** @import { ComponentClientTransformState } from '../types.js' */
/** @import { Node } from './types.js' */
import { TEMPLATE_USE_MATHML, TEMPLATE_USE_SVG } from '../../../../../constants.js';
import { dev, locator } from '../../../../state.js';
import * as b from '../../../../utils/builders.js';
/**
* @param {Node[]} nodes
*/
function build_locations(nodes) {
const array = b.array([]);
for (const node of nodes) {
if (node.type !== 'element') continue;
const { line, column } = /** @type {Location} */ (locator(node.start));
const expression = b.array([b.literal(line), b.literal(column)]);
const children = build_locations(node.children);
if (children.elements.length > 0) {
expression.elements.push(children);
}
array.elements.push(expression);
}
return array;
}
/**
* @param {ComponentClientTransformState} state
* @param {Namespace} namespace
* @param {number} [flags]
*/
export function transform_template(state, namespace, flags = 0) {
const tree = state.options.fragments === 'tree';
const expression = tree ? state.template.as_tree() : state.template.as_html();
if (tree) {
if (namespace === 'svg') flags |= TEMPLATE_USE_SVG;
if (namespace === 'mathml') flags |= TEMPLATE_USE_MATHML;
}
let call = b.call(
tree ? `$.from_tree` : `$.from_${namespace}`,
expression,
flags ? b.literal(flags) : undefined
);
if (state.template.contains_script_tag) {
call = b.call(`$.with_script`, call);
}
if (dev) {
call = b.call(
'$.add_locations',
call,
b.member(b.id(state.analysis.name), '$.FILENAME', true),
build_locations(state.template.nodes)
);
}
return call;
}

@ -0,0 +1,162 @@
/** @import { AST } from '#compiler' */
/** @import { Node, Element } from './types'; */
import { escape_html } from '../../../../../escaping.js';
import { is_void } from '../../../../../utils.js';
import * as b from '#compiler/builders';
import fix_attribute_casing from './fix-attribute-casing.js';
import { regex_starts_with_newline } from '../../../patterns.js';
export class Template {
/**
* `true` if HTML template contains a `<script>` tag. In this case we need to invoke a special
* template instantiation function (see `create_fragment_with_script_from_html` for more info)
*/
contains_script_tag = false;
/** `true` if the HTML template needs to be instantiated with `importNode` */
needs_import_node = false;
/** @type {Node[]} */
nodes = [];
/** @type {Node[][]} */
#stack = [this.nodes];
/** @type {Element | undefined} */
#element;
#fragment = this.nodes;
/**
* @param {string} name
* @param {number} start
*/
push_element(name, start) {
this.#element = {
type: 'element',
name,
attributes: {},
children: [],
start
};
this.#fragment.push(this.#element);
this.#fragment = /** @type {Element} */ (this.#element).children;
this.#stack.push(this.#fragment);
}
/** @param {string} [data] */
push_comment(data) {
this.#fragment.push({ type: 'comment', data });
}
/** @param {AST.Text[]} nodes */
push_text(nodes) {
this.#fragment.push({ type: 'text', nodes });
}
pop_element() {
this.#stack.pop();
this.#fragment = /** @type {Node[]} */ (this.#stack.at(-1));
}
/**
* @param {string} key
* @param {string | undefined} value
*/
set_prop(key, value) {
/** @type {Element} */ (this.#element).attributes[key] = value;
}
as_html() {
return b.template([b.quasi(this.nodes.map(stringify).join(''), true)], []);
}
as_tree() {
// if the first item is a comment we need to add another comment for effect.start
if (this.nodes[0].type === 'comment') {
this.nodes.unshift({ type: 'comment', data: undefined });
}
return b.array(this.nodes.map(objectify));
}
}
/**
* @param {Node} item
*/
function stringify(item) {
if (item.type === 'text') {
return item.nodes.map((node) => node.raw).join('');
}
if (item.type === 'comment') {
return item.data ? `<!--${item.data}-->` : '<!>';
}
let str = `<${item.name}`;
for (const key in item.attributes) {
const value = item.attributes[key];
str += ` ${key}`;
if (value !== undefined) str += `="${escape_html(value, true)}"`;
}
if (is_void(item.name)) {
str += '/>'; // XHTML compliance
} else {
str += `>`;
str += item.children.map(stringify).join('');
str += `</${item.name}>`;
}
return str;
}
/** @param {Node} item */
function objectify(item) {
if (item.type === 'text') {
return b.literal(item.nodes.map((node) => node.data).join(''));
}
if (item.type === 'comment') {
return item.data ? b.array([b.literal(`// ${item.data}`)]) : null;
}
const element = b.array([b.literal(item.name)]);
const attributes = b.object([]);
for (const key in item.attributes) {
const value = item.attributes[key];
attributes.properties.push(
b.prop(
'init',
b.key(fix_attribute_casing(key)),
value === undefined ? b.void0 : b.literal(value)
)
);
}
if (attributes.properties.length > 0 || item.children.length > 0) {
element.elements.push(attributes.properties.length > 0 ? attributes : b.null);
}
if (item.children.length > 0) {
const children = item.children.map(objectify);
element.elements.push(...children);
// special case — strip leading newline from `<pre>` and `<textarea>`
if (item.name === 'pre' || item.name === 'textarea') {
const first = children[0];
if (first?.type === 'Literal') {
first.value = /** @type {string} */ (first.value).replace(regex_starts_with_newline, '');
}
}
}
return element;
}

@ -0,0 +1,22 @@
import type { AST } from '#compiler';
export interface Element {
type: 'element';
name: string;
attributes: Record<string, string | undefined>;
children: Node[];
/** used for populating __svelte_meta */
start: number;
}
export interface Text {
type: 'text';
nodes: AST.Text[];
}
export interface Comment {
type: 'comment';
data: string | undefined;
}
export type Node = Element | Text | Comment;

@ -3,7 +3,6 @@ import type {
Statement,
LabeledStatement,
Identifier,
PrivateIdentifier,
Expression,
AssignmentExpression,
UpdateExpression,
@ -12,12 +11,9 @@ import type {
import type { AST, Namespace, ValidatedCompileOptions } from '#compiler';
import type { TransformState } from '../types.js';
import type { ComponentAnalysis } from '../../types.js';
import type { SourceLocation } from '#shared';
import type { Template } from './transform-template/template.js';
export interface ClientTransformState extends TransformState {
readonly private_state: Map<string, StateField>;
readonly public_state: Map<string, StateField>;
/**
* `true` if the current lexical scope belongs to a class constructor. this allows
* us to rewrite `this.foo` as `this.#foo.value`
@ -56,26 +52,10 @@ export interface ComponentClientTransformState extends ClientTransformState {
/** Expressions used inside the render effect */
readonly expressions: Expression[];
/** The HTML template string */
readonly template: Array<string | Expression>;
readonly locations: SourceLocation[];
readonly template: Template;
readonly metadata: {
namespace: Namespace;
bound_contenteditable: boolean;
/**
* Stuff that is set within the children of one `Fragment` visitor that is relevant
* to said fragment. Shouldn't be destructured or otherwise spread unless inside the
* `Fragment` visitor to keep the object reference intact (it's also nested
* within `metadata` for this reason).
*/
context: {
/** `true` if the HTML template needs to be instantiated with `importNode` */
template_needs_import_node: boolean;
/**
* `true` if HTML template contains a `<script>` tag. In this case we need to invoke a special
* template instantiation function (see `create_fragment_with_script_from_html` for more info)
*/
template_contains_script_tag: boolean;
};
};
readonly preserve_whitespace: boolean;
@ -94,11 +74,6 @@ export interface ComponentClientTransformState extends ClientTransformState {
readonly module_level_snippets: VariableDeclaration[];
}
export interface StateField {
kind: 'state' | 'raw_state' | 'derived' | 'derived_by';
id: PrivateIdentifier;
}
export type Context = import('zimmerframe').Context<AST.SvelteNode, ClientTransformState>;
export type Visitors = import('zimmerframe').Visitors<AST.SvelteNode, any>;

@ -11,6 +11,8 @@ import { dev, locate_node } from '../../../../state.js';
import { should_proxy } from '../utils.js';
import { visit_assignment_expression } from '../../shared/assignments.js';
import { validate_mutation } from './shared/utils.js';
import { get_rune } from '../../../scope.js';
import { get_name } from '../../../nodes.js';
/**
* @param {AssignmentExpression} node
@ -50,27 +52,53 @@ const callees = {
* @returns {Expression | null}
*/
function build_assignment(operator, left, right, context) {
// Handle class private/public state assignment cases
if (
context.state.analysis.runes &&
left.type === 'MemberExpression' &&
left.property.type === 'PrivateIdentifier'
) {
const private_state = context.state.private_state.get(left.property.name);
if (context.state.analysis.runes && left.type === 'MemberExpression') {
const name = get_name(left.property);
const field = name && context.state.state_fields.get(name);
if (field) {
// special case — state declaration in class constructor
if (field.node.type === 'AssignmentExpression' && left === field.node.left) {
const rune = get_rune(right, context.state.scope);
if (rune) {
const child_state = {
...context.state,
in_constructor: rune !== '$derived' && rune !== '$derived.by'
};
let value = /** @type {Expression} */ (context.visit(right, child_state));
if (dev) {
const declaration = context.path.findLast(
(parent) => parent.type === 'ClassDeclaration' || parent.type === 'ClassExpression'
);
value = b.call(
'$.tag',
value,
b.literal(`${declaration?.id?.name ?? '[class]'}.${name}`)
);
}
if (private_state !== undefined) {
return b.assignment(operator, b.member(b.this, field.key), value);
}
}
// special case — assignment to private state field
if (left.property.type === 'PrivateIdentifier') {
let value = /** @type {Expression} */ (
context.visit(build_assignment_value(operator, left, right))
);
const needs_proxy =
private_state.kind === 'state' &&
field.type === '$state' &&
is_non_coercive_operator(operator) &&
should_proxy(value, context.state.scope);
return b.call('$.set', left, value, needs_proxy && b.true);
}
}
}
let object = left;

@ -0,0 +1,21 @@
/** @import { Expression } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */
import * as b from '../../../../utils/builders.js';
/**
* @param {AST.AttachTag} node
* @param {ComponentContext} context
*/
export function AttachTag(node, context) {
context.state.init.push(
b.stmt(
b.call(
'$.attach',
context.state.node,
b.thunk(/** @type {Expression} */ (context.visit(node.expression)))
)
)
);
context.next();
}

@ -11,7 +11,7 @@ import { get_value } from './shared/declarations.js';
* @param {ComponentContext} context
*/
export function AwaitBlock(node, context) {
context.state.template.push('<!>');
context.state.template.push_comment();
// Visit {#await <expression>} first to ensure that scopes are in the correct order
const expression = b.thunk(/** @type {Expression} */ (context.visit(node.expression)));

@ -4,19 +4,52 @@ import { dev, is_ignored } from '../../../../state.js';
import * as b from '#compiler/builders';
import { get_rune } from '../../../scope.js';
import { transform_inspect_rune } from '../../utils.js';
import { should_proxy } from '../utils.js';
/**
* @param {CallExpression} node
* @param {Context} context
*/
export function CallExpression(node, context) {
switch (get_rune(node, context.state.scope)) {
const rune = get_rune(node, context.state.scope);
switch (rune) {
case '$host':
return b.id('$$props.$$host');
case '$effect.tracking':
return b.call('$.effect_tracking');
// transform state field assignments in constructors
case '$state':
case '$state.raw': {
let arg = node.arguments[0];
/** @type {Expression | undefined} */
let value = undefined;
if (arg) {
value = /** @type {Expression} */ (context.visit(node.arguments[0]));
if (
rune === '$state' &&
should_proxy(/** @type {Expression} */ (arg), context.state.scope)
) {
value = b.call('$.proxy', value);
}
}
return b.call('$.state', value);
}
case '$derived':
case '$derived.by': {
let fn = /** @type {Expression} */ (context.visit(node.arguments[0]));
if (rune === '$derived') fn = b.thunk(fn);
return b.call('$.derived', fn);
}
case '$state.snapshot':
return b.call(
'$.snapshot',

@ -1,184 +1,111 @@
/** @import { ClassBody, Expression, Identifier, Literal, MethodDefinition, PrivateIdentifier, PropertyDefinition } from 'estree' */
/** @import { Context, StateField } from '../types' */
/** @import { CallExpression, ClassBody, ClassDeclaration, ClassExpression, MethodDefinition, PropertyDefinition, StaticBlock } from 'estree' */
/** @import { StateField } from '#compiler' */
/** @import { Context } from '../types' */
import * as b from '#compiler/builders';
import { regex_invalid_identifier_chars } from '../../../patterns.js';
import { get_rune } from '../../../scope.js';
import { should_proxy } from '../utils.js';
import { dev } from '../../../../state.js';
import { get_parent } from '../../../../utils/ast.js';
import { get_name } from '../../../nodes.js';
/**
* @param {ClassBody} node
* @param {Context} context
*/
export function ClassBody(node, context) {
if (!context.state.analysis.runes) {
const state_fields = context.state.analysis.classes.get(node);
if (!state_fields) {
// in legacy mode, do nothing
context.next();
return;
}
/** @type {Map<string, StateField>} */
const public_state = new Map();
/** @type {Array<MethodDefinition | PropertyDefinition | StaticBlock>} */
const body = [];
/** @type {Map<string, StateField>} */
const private_state = new Map();
const child_state = { ...context.state, state_fields };
/** @type {Map<(MethodDefinition|PropertyDefinition)["key"], string>} */
const definition_names = new Map();
for (const [name, field] of state_fields) {
if (name[0] === '#') {
continue;
}
/** @type {string[]} */
const private_ids = [];
// insert backing fields for stuff declared in the constructor
if (field.node.type === 'AssignmentExpression') {
const member = b.member(b.this, field.key);
for (const definition of node.body) {
if (
(definition.type === 'PropertyDefinition' || definition.type === 'MethodDefinition') &&
(definition.key.type === 'Identifier' ||
definition.key.type === 'PrivateIdentifier' ||
definition.key.type === 'Literal')
) {
const type = definition.key.type;
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);
if (definition.value?.type === 'CallExpression') {
const rune = get_rune(definition.value, context.state.scope);
if (
rune === '$state' ||
rune === '$state.raw' ||
rune === '$derived' ||
rune === '$derived.by'
) {
/** @type {StateField} */
const field = {
kind:
rune === '$state'
? 'state'
: rune === '$state.raw'
? 'raw_state'
: rune === '$derived.by'
? 'derived_by'
: 'derived',
// @ts-expect-error this is set in the next pass
id: is_private ? definition.key : null
};
if (is_private) {
private_state.set(name, field);
} else {
public_state.set(name, field);
}
}
}
}
}
const should_proxy = field.type === '$state' && true; // TODO
// each `foo = $state()` needs a backing `#foo` field
for (const [name, field] of public_state) {
let deconflicted = name;
while (private_ids.includes(deconflicted)) {
deconflicted = '_' + deconflicted;
}
const key = b.key(name);
private_ids.push(deconflicted);
field.id = b.private_id(deconflicted);
}
body.push(
b.prop_def(field.key, null),
/** @type {Array<MethodDefinition | PropertyDefinition>} */
const body = [];
b.method('get', key, [], [b.return(b.call('$.get', member))]),
const child_state = { ...context.state, public_state, private_state };
b.method(
'set',
key,
[b.id('value')],
[b.stmt(b.call('$.set', member, b.id('value'), should_proxy && b.true))]
)
);
}
}
const declaration = /** @type {ClassDeclaration | ClassExpression} */ (
get_parent(context.path, -1)
);
// Replace parts of the class body
for (const definition of node.body) {
if (
definition.type === 'PropertyDefinition' &&
(definition.key.type === 'Identifier' ||
definition.key.type === 'PrivateIdentifier' ||
definition.key.type === 'Literal')
) {
const name = definition_names.get(definition.key);
if (!name) continue;
const is_private = definition.key.type === 'PrivateIdentifier';
const field = (is_private ? private_state : public_state).get(name);
if (definition.value?.type === 'CallExpression' && field !== undefined) {
let value = null;
if (definition.value.arguments.length > 0) {
const init = /** @type {Expression} **/ (
context.visit(definition.value.arguments[0], child_state)
if (definition.type !== 'PropertyDefinition') {
body.push(
/** @type {MethodDefinition | StaticBlock} */ (context.visit(definition, child_state))
);
continue;
}
value =
field.kind === 'state'
? b.call(
'$.state',
should_proxy(init, context.state.scope) ? b.call('$.proxy', init) : init
)
: field.kind === 'raw_state'
? b.call('$.state', init)
: field.kind === 'derived_by'
? b.call('$.derived', init)
: b.call('$.derived', b.thunk(init));
} else {
// if no arguments, we know it's state as `$derived()` is a compile error
value = b.call('$.state');
const name = get_name(definition.key);
const field = name && /** @type {StateField} */ (state_fields.get(name));
if (!field) {
body.push(/** @type {PropertyDefinition} */ (context.visit(definition, child_state)));
continue;
}
if (is_private) {
body.push(b.prop_def(field.id, value));
} else {
// #foo;
const member = b.member(b.this, field.id);
body.push(b.prop_def(field.id, value));
if (name[0] === '#') {
let value = definition.value
? /** @type {CallExpression} */ (context.visit(definition.value, child_state))
: undefined;
// get foo() { return this.#foo; }
body.push(b.method('get', definition.key, [], [b.return(b.call('$.get', member))]));
if (dev && field.node === definition) {
value = b.call('$.tag', value, b.literal(`${declaration.id?.name ?? '[class]'}.${name}`));
}
body.push(b.prop_def(definition.key, value));
} else if (field.node === definition) {
let call = /** @type {CallExpression} */ (context.visit(field.value, child_state));
// set foo(value) { this.#foo = value; }
const val = b.id('value');
if (dev) {
call = b.call('$.tag', call, b.literal(`${declaration.id?.name ?? '[class]'}.${name}`));
}
const member = b.member(b.this, field.key);
const should_proxy = field.type === '$state' && true; // TODO
body.push(
b.prop_def(field.key, call),
b.method('get', definition.key, [], [b.return(b.call('$.get', member))]),
b.method(
'set',
definition.key,
[val],
[b.stmt(b.call('$.set', member, val, field.kind === 'state' && b.true))]
[b.id('value')],
[b.stmt(b.call('$.set', member, b.id('value'), should_proxy && b.true))]
)
);
}
continue;
}
}
body.push(/** @type {MethodDefinition} **/ (context.visit(definition, child_state)));
}
return { ...node, body };
}
/**
* @param {Identifier | PrivateIdentifier | Literal} node
* @param {Map<string, StateField>} public_state
*/
function get_name(node, public_state) {
if (node.type === 'Literal') {
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;
}
}

@ -7,5 +7,5 @@
*/
export function Comment(node, context) {
// We'll only get here if comments are not filtered out, which they are unless preserveComments is true
context.state.template.push(`<!--${node.data}-->`);
context.state.template.push_comment(node.data);
}

@ -1,7 +1,6 @@
/** @import { Expression } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */
import * as b from '#compiler/builders';
import { regex_is_valid_identifier } from '../../../patterns.js';
import { build_component } from './shared/component.js';
/**
@ -9,24 +8,12 @@ import { build_component } from './shared/component.js';
* @param {ComponentContext} context
*/
export function Component(node, context) {
if (node.metadata.dynamic) {
// Handle dynamic references to what seems like static inline components
const component = build_component(node, '$$component', context, b.id('$$anchor'));
context.state.init.push(
b.stmt(
b.call(
'$.component',
context.state.node,
// TODO use untrack here to not update when binding changes?
// Would align with Svelte 4 behavior, but it's arguably nicer/expected to update this
b.thunk(/** @type {Expression} */ (context.visit(b.member_id(node.name)))),
b.arrow([b.id('$$anchor'), b.id('$$component')], b.block([component]))
)
)
const component = build_component(
node,
// if it's not dynamic we will just use the node name, if it is dynamic we will use the node name
// only if it's a valid identifier, otherwise we will use a default name
!node.metadata.dynamic || regex_is_valid_identifier.test(node.name) ? node.name : '$$component',
context
);
return;
}
const component = build_component(node, node.name, context);
context.state.init.push(component);
}

@ -1,4 +1,4 @@
/** @import { BlockStatement, Expression, Identifier, Pattern, Statement } from 'estree' */
/** @import { BlockStatement, Expression, Identifier, Pattern, SequenceExpression, Statement } from 'estree' */
/** @import { AST, Binding } from '#compiler' */
/** @import { ComponentContext } from '../types' */
/** @import { Scope } from '../../../scope' */
@ -32,7 +32,7 @@ export function EachBlock(node, context) {
);
if (!each_node_meta.is_controlled) {
context.state.template.push('<!>');
context.state.template.push_comment();
}
let flags = 0;
@ -106,16 +106,6 @@ export function EachBlock(node, context) {
}
}
// Legacy mode: find the parent each blocks which contain the arrays to invalidate
const indirect_dependencies = collect_parent_each_blocks(context).flatMap((block) => {
const array = /** @type {Expression} */ (context.visit(block.expression));
const transitive_dependencies = build_transitive_dependencies(
block.metadata.expression.dependencies,
context
);
return [array, ...transitive_dependencies];
});
/** @type {Identifier | null} */
let collection_id = null;
@ -129,18 +119,6 @@ export function EachBlock(node, context) {
}
}
if (collection_id) {
indirect_dependencies.push(b.call(collection_id));
} else {
indirect_dependencies.push(collection);
const transitive_dependencies = build_transitive_dependencies(
each_node_meta.expression.dependencies,
context
);
indirect_dependencies.push(...transitive_dependencies);
}
const child_state = {
...context.state,
transform: { ...context.state.transform },
@ -181,19 +159,51 @@ export function EachBlock(node, context) {
/** @type {Statement[]} */
const declarations = [];
const invalidate = b.call(
'$.invalidate_inner_signals',
b.thunk(b.sequence(indirect_dependencies))
);
const invalidate_store = store_to_invalidate
? b.call('$.invalidate_store', b.id('$$stores'), b.literal(store_to_invalidate))
: undefined;
/** @type {Expression[]} */
const sequence = [];
if (!context.state.analysis.runes) sequence.push(invalidate);
if (invalidate_store) sequence.push(invalidate_store);
if (!context.state.analysis.runes) {
/** @type {Set<Identifier>} */
const transitive_deps = new Set();
if (collection_id) {
transitive_deps.add(collection_id);
child_state.transform[collection_id.name] = { read: b.call };
} else {
for (const binding of each_node_meta.transitive_deps) {
transitive_deps.add(binding.node);
}
}
for (const block of collect_parent_each_blocks(context)) {
for (const binding of block.metadata.transitive_deps) {
transitive_deps.add(binding.node);
}
}
if (transitive_deps.size > 0) {
const invalidate = b.call(
'$.invalidate_inner_signals',
b.thunk(
b.sequence(
[...transitive_deps].map(
(node) => /** @type {Expression} */ (context.visit({ ...node }, child_state))
)
)
)
);
sequence.push(invalidate);
}
}
if (invalidate_store) {
sequence.push(invalidate_store);
}
if (node.context?.type === 'Identifier') {
const binding = /** @type {Binding} */ (context.state.scope.get(node.context.name));
@ -234,13 +244,21 @@ export function EachBlock(node, context) {
} else if (node.context) {
const unwrapped = (flags & EACH_ITEM_REACTIVE) !== 0 ? b.call('$.get', item) : item;
for (const path of extract_paths(node.context)) {
const { inserts, paths } = extract_paths(node.context, unwrapped);
for (const { id, value } of inserts) {
id.name = context.state.scope.generate('$$array');
child_state.transform[id.name] = { read: get_value };
const expression = /** @type {Expression} */ (context.visit(b.thunk(value), child_state));
declarations.push(b.var(id, b.call('$.derived', expression)));
}
for (const path of paths) {
const name = /** @type {Identifier} */ (path.node).name;
const needs_derived = path.has_default_value; // to ensure that default value is only called once
const fn = b.thunk(
/** @type {Expression} */ (context.visit(path.expression?.(unwrapped), child_state))
);
const fn = b.thunk(/** @type {Expression} */ (context.visit(path.expression, child_state)));
declarations.push(b.let(path.node, needs_derived ? b.call('$.derived_safe_equal', fn) : fn));
@ -249,7 +267,7 @@ export function EachBlock(node, context) {
child_state.transform[name] = {
read,
assign: (_, value) => {
const left = /** @type {Pattern} */ (path.update_expression(unwrapped));
const left = /** @type {Pattern} */ (path.update_expression);
return b.sequence([b.assignment('=', left, value), ...sequence]);
},
mutate: (_, mutation) => {
@ -321,41 +339,3 @@ export function EachBlock(node, context) {
function collect_parent_each_blocks(context) {
return /** @type {AST.EachBlock[]} */ (context.path.filter((node) => node.type === 'EachBlock'));
}
/**
* @param {Set<Binding>} references
* @param {ComponentContext} context
*/
function build_transitive_dependencies(references, context) {
/** @type {Set<Binding>} */
const dependencies = new Set();
for (const ref of references) {
const deps = collect_transitive_dependencies(ref);
for (const dep of deps) {
dependencies.add(dep);
}
}
return [...dependencies].map((dep) => build_getter({ ...dep.node }, context.state));
}
/**
* @param {Binding} binding
* @param {Set<Binding>} seen
* @returns {Binding[]}
*/
function collect_transitive_dependencies(binding, seen = new Set()) {
if (binding.kind !== 'legacy_reactive') return [];
for (const dep of binding.legacy_dependencies) {
if (!seen.has(dep)) {
seen.add(dep);
for (const transitive_dep of collect_transitive_dependencies(dep, seen)) {
seen.add(transitive_dep);
}
}
}
return [...seen];
}

@ -1,14 +1,13 @@
/** @import { Expression, Identifier, Statement, TemplateElement } from 'estree' */
/** @import { AST, Namespace } from '#compiler' */
/** @import { SourceLocation } from '#shared' */
/** @import { Expression, Statement } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentClientTransformState, ComponentContext } from '../types' */
import { TEMPLATE_FRAGMENT, TEMPLATE_USE_IMPORT_NODE } from '../../../../../constants.js';
import { dev } from '../../../../state.js';
import * as b from '#compiler/builders';
import { sanitize_template_string } from '../../../../utils/sanitize_template_string.js';
import { clean_nodes, infer_namespace } from '../../utils.js';
import { transform_template } from '../transform-template/index.js';
import { process_children } from './shared/fragment.js';
import { build_render_statement } from './shared/utils.js';
import { Template } from '../transform-template/template.js';
/**
* @param {AST.Fragment} node
@ -18,7 +17,7 @@ export function Fragment(node, context) {
// Creates a new block which looks roughly like this:
// ```js
// // hoisted:
// const block_name = $.template(`...`);
// const block_name = $.from_html(`...`);
//
// // for the main block:
// const id = block_name();
@ -67,14 +66,9 @@ export function Fragment(node, context) {
update: [],
expressions: [],
after_update: [],
template: [],
locations: [],
template: new Template(),
transform: { ...context.state.transform },
metadata: {
context: {
template_needs_import_node: false,
template_contains_script_tag: false
},
namespace,
bound_contenteditable: context.state.metadata.bound_contenteditable
}
@ -89,24 +83,6 @@ export function Fragment(node, context) {
body.push(b.stmt(b.call('$.next')));
}
/**
* @param {Identifier} template_name
* @param {Expression[]} args
*/
const add_template = (template_name, args) => {
let call = b.call(get_template_function(namespace, state), ...args);
if (dev) {
call = b.call(
'$.add_locations',
call,
b.member(b.id(context.state.analysis.name), '$.FILENAME', true),
build_locations(state.locations)
);
}
context.state.hoisted.push(b.var(template_name, call));
};
if (is_single_element) {
const element = /** @type {AST.RegularElement} */ (trimmed[0]);
@ -117,14 +93,10 @@ export function Fragment(node, context) {
node: id
});
/** @type {Expression[]} */
const args = [join_template(state.template)];
if (state.metadata.context.template_needs_import_node) {
args.push(b.literal(TEMPLATE_USE_IMPORT_NODE));
}
let flags = state.template.needs_import_node ? TEMPLATE_USE_IMPORT_NODE : undefined;
add_template(template_name, args);
const template = transform_template(state, namespace, flags);
state.hoisted.push(b.var(template_name, template));
body.push(b.var(id, b.call(template_name)));
close = b.stmt(b.call('$.append', b.id('$$anchor'), id));
@ -164,15 +136,16 @@ export function Fragment(node, context) {
let flags = TEMPLATE_FRAGMENT;
if (state.metadata.context.template_needs_import_node) {
if (state.template.needs_import_node) {
flags |= TEMPLATE_USE_IMPORT_NODE;
}
if (state.template.length === 1 && state.template[0] === '<!>') {
if (state.template.nodes.length === 1 && state.template.nodes[0].type === 'comment') {
// special case — we can use `$.comment` instead of creating a unique template
body.push(b.var(id, b.call('$.comment')));
} else {
add_template(template_name, [join_template(state.template), b.literal(flags)]);
const template = transform_template(state, namespace, flags);
state.hoisted.push(b.var(template_name, template));
body.push(b.var(id, b.call(template_name)));
}
@ -199,86 +172,3 @@ export function Fragment(node, context) {
return b.block(body);
}
/**
* @param {Array<string | Expression>} items
*/
function join_template(items) {
let quasi = b.quasi('');
const template = b.template([quasi], []);
/**
* @param {Expression} expression
*/
function push(expression) {
if (expression.type === 'TemplateLiteral') {
for (let i = 0; i < expression.expressions.length; i += 1) {
const q = expression.quasis[i];
const e = expression.expressions[i];
quasi.value.cooked += /** @type {string} */ (q.value.cooked);
push(e);
}
const last = /** @type {TemplateElement} */ (expression.quasis.at(-1));
quasi.value.cooked += /** @type {string} */ (last.value.cooked);
} else if (expression.type === 'Literal') {
/** @type {string} */ (quasi.value.cooked) += expression.value;
} else {
template.expressions.push(expression);
template.quasis.push((quasi = b.quasi('')));
}
}
for (const item of items) {
if (typeof item === 'string') {
quasi.value.cooked += item;
} else {
push(item);
}
}
for (const quasi of template.quasis) {
quasi.value.raw = sanitize_template_string(/** @type {string} */ (quasi.value.cooked));
}
quasi.tail = true;
return template;
}
/**
*
* @param {Namespace} namespace
* @param {ComponentClientTransformState} state
* @returns
*/
function get_template_function(namespace, state) {
const contains_script_tag = state.metadata.context.template_contains_script_tag;
return namespace === 'svg'
? contains_script_tag
? '$.svg_template_with_script'
: '$.ns_template'
: namespace === 'mathml'
? '$.mathml_template'
: contains_script_tag
? '$.template_with_script'
: '$.template';
}
/**
* @param {SourceLocation[]} locations
*/
function build_locations(locations) {
return b.array(
locations.map((loc) => {
const expression = b.array([b.literal(loc[0]), b.literal(loc[1])]);
if (loc.length === 3) {
expression.elements.push(build_locations(loc[2]));
}
return expression;
})
);
}

@ -9,7 +9,7 @@ import * as b from '#compiler/builders';
* @param {ComponentContext} context
*/
export function HtmlTag(node, context) {
context.state.template.push('<!>');
context.state.template.push_comment();
const expression = /** @type {Expression} */ (context.visit(node.expression));

@ -8,7 +8,7 @@ import * as b from '#compiler/builders';
* @param {ComponentContext} context
*/
export function IfBlock(node, context) {
context.state.template.push('<!>');
context.state.template.push_comment();
const statements = [];
const consequent = /** @type {BlockStatement} */ (context.visit(node.consequent));

@ -8,7 +8,7 @@ import * as b from '#compiler/builders';
* @param {ComponentContext} context
*/
export function KeyBlock(node, context) {
context.state.template.push('<!>');
context.state.template.push_comment();
const key = /** @type {Expression} */ (context.visit(node.expression));
const body = /** @type {Expression} */ (context.visit(node.fragment));

@ -9,9 +9,11 @@ import * as b from '#compiler/builders';
export function MemberExpression(node, context) {
// rewrite `this.#foo` as `this.#foo.v` inside a constructor
if (node.property.type === 'PrivateIdentifier') {
const field = context.state.private_state.get(node.property.name);
const field = context.state.state_fields.get('#' + node.property.name);
if (field) {
return context.state.in_constructor && (field.kind === 'raw_state' || field.kind === 'state')
return context.state.in_constructor &&
(field.type === '$state.raw' || field.type === '$state')
? b.member(node, 'v')
: b.call('$.get', node);
}

@ -1,17 +1,14 @@
/** @import { ArrayExpression, Expression, ExpressionStatement, Identifier, MemberExpression, ObjectExpression } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { SourceLocation } from '#shared' */
/** @import { ComponentClientTransformState, ComponentContext } from '../types' */
/** @import { Scope } from '../../../scope' */
import {
cannot_be_set_statically,
is_boolean_attribute,
is_dom_property,
is_load_error_element,
is_void
is_load_error_element
} from '../../../../../utils.js';
import { escape_html } from '../../../../../escaping.js';
import { dev, is_ignored, locator } from '../../../../state.js';
import { is_ignored } from '../../../../state.js';
import { is_event_attribute, is_text_attribute } from '../../../../utils/ast.js';
import * as b from '#compiler/builders';
import { is_custom_element_node } from '../../../nodes.js';
@ -20,7 +17,7 @@ import { build_getter } from '../utils.js';
import {
get_attribute_name,
build_attribute_value,
build_set_attributes,
build_attribute_effect,
build_set_class,
build_set_style
} from './shared/element.js';
@ -39,40 +36,24 @@ import { visit_event_attribute } from './shared/events.js';
* @param {ComponentContext} context
*/
export function RegularElement(node, context) {
/** @type {SourceLocation} */
let location = [-1, -1];
if (dev) {
const loc = locator(node.start);
if (loc) {
location[0] = loc.line;
location[1] = loc.column;
context.state.locations.push(location);
}
}
context.state.template.push_element(node.name, node.start);
if (node.name === 'noscript') {
context.state.template.push('<noscript></noscript>');
context.state.template.pop_element();
return;
}
const is_custom_element = is_custom_element_node(node);
if (node.name === 'video' || is_custom_element) {
// cloneNode is faster, but it does not instantiate the underlying class of the
// custom element until the template is connected to the dom, which would
// cause problems when setting properties on the custom element.
// Therefore we need to use importNode instead, which doesn't have this caveat.
// Additionally, Webkit browsers need importNode for video elements for autoplay
// to work correctly.
context.state.metadata.context.template_needs_import_node = true;
}
context.state.template.needs_import_node ||= node.name === 'video' || is_custom_element;
if (node.name === 'script') {
context.state.metadata.context.template_contains_script_tag = true;
}
context.state.template.push(`<${node.name}`);
context.state.template.contains_script_tag ||= node.name === 'script';
/** @type {Array<AST.Attribute | AST.SpreadAttribute>} */
const attributes = [];
@ -83,7 +64,7 @@ export function RegularElement(node, context) {
/** @type {AST.StyleDirective[]} */
const style_directives = [];
/** @type {Array<AST.AnimateDirective | AST.BindDirective | AST.OnDirective | AST.TransitionDirective | AST.UseDirective>} */
/** @type {Array<AST.AnimateDirective | AST.BindDirective | AST.OnDirective | AST.TransitionDirective | AST.UseDirective | AST.AttachTag>} */
const other_directives = [];
/** @type {ExpressionStatement[]} */
@ -110,7 +91,7 @@ export function RegularElement(node, context) {
const { value } = build_attribute_value(attribute.value, context);
if (value.type === 'Literal' && typeof value.value === 'string') {
context.state.template.push(` is="${escape_html(value.value, true)}"`);
context.state.template.set_prop('is', value.value);
continue;
}
}
@ -153,6 +134,10 @@ export function RegularElement(node, context) {
has_use = true;
other_directives.push(attribute);
break;
case 'AttachTag':
other_directives.push(attribute);
break;
}
}
@ -216,37 +201,7 @@ export function RegularElement(node, context) {
const node_id = context.state.node;
if (has_spread) {
const attributes_id = b.id(context.state.scope.generate('attributes'));
build_set_attributes(
attributes,
class_directives,
style_directives,
context,
node,
node_id,
attributes_id
);
// If value binding exists, that one takes care of calling $.init_select
if (node.name === 'select' && !bindings.has('value')) {
context.state.init.push(
b.stmt(b.call('$.init_select', node_id, b.thunk(b.member(attributes_id, 'value'))))
);
context.state.update.push(
b.if(
b.binary('in', b.literal('value'), attributes_id),
b.block([
// This ensures a one-way street to the DOM in case it's <select {value}>
// and not <select bind:value>. We need it in addition to $.init_select
// because the select value is not reflected as an attribute, so the
// mutation observer wouldn't notice.
b.stmt(b.call('$.select_option', node_id, b.member(attributes_id, 'value')))
])
)
);
}
build_attribute_effect(attributes, class_directives, style_directives, context, node, node_id);
} else {
/** If true, needs `__value` for inputs */
const needs_special_value_handling =
@ -286,12 +241,9 @@ export function RegularElement(node, context) {
}
if (name !== 'class' || value) {
context.state.template.push(
` ${attribute.name}${
is_boolean_attribute(name) && value === true
? ''
: `="${value === true ? '' : escape_html(value, true)}"`
}`
context.state.template.set_prop(
attribute.name,
is_boolean_attribute(name) && value === true ? undefined : value === true ? '' : value
);
}
} else if (name === 'autofocus') {
@ -308,7 +260,8 @@ export function RegularElement(node, context) {
const { value, has_state } = build_attribute_value(
attribute.value,
context,
(value, metadata) => (metadata.has_call ? get_expression_id(context.state, value) : value)
(value, metadata) =>
metadata.has_call ? get_expression_id(context.state.expressions, value) : value
);
const update = build_element_attribute_update(node, node_id, name, value, attributes);
@ -325,8 +278,6 @@ export function RegularElement(node, context) {
context.state.after_update.push(b.stmt(b.call('$.replay_events', node_id)));
}
context.state.template.push('>');
const metadata = {
...context.state.metadata,
namespace: determine_namespace_for_children(node, context.state.metadata.namespace)
@ -348,7 +299,6 @@ export function RegularElement(node, context) {
const state = {
...context.state,
metadata,
locations: [],
scope: /** @type {Scope} */ (context.state.scopes.get(node.fragment)),
preserve_whitespace:
context.state.preserve_whitespace || node.name === 'pre' || node.name === 'textarea'
@ -442,14 +392,7 @@ export function RegularElement(node, context) {
context.state.update.push(b.stmt(b.assignment('=', dir, dir)));
}
if (state.locations.length > 0) {
// @ts-expect-error
location.push(state.locations);
}
if (!is_void(node.name)) {
context.state.template.push(`</${node.name}>`);
}
context.state.template.pop_element();
}
/**
@ -510,10 +453,11 @@ function setup_select_synchronization(value_binding, context) {
/**
* @param {AST.ClassDirective[]} class_directives
* @param {Expression[]} expressions
* @param {ComponentContext} context
* @return {ObjectExpression | Identifier}
*/
export function build_class_directives_object(class_directives, context) {
export function build_class_directives_object(class_directives, expressions, context) {
let properties = [];
let has_call_or_state = false;
@ -525,15 +469,16 @@ export function build_class_directives_object(class_directives, context) {
const directives = b.object(properties);
return has_call_or_state ? get_expression_id(context.state, directives) : directives;
return has_call_or_state ? get_expression_id(expressions, directives) : directives;
}
/**
* @param {AST.StyleDirective[]} style_directives
* @param {Expression[]} expressions
* @param {ComponentContext} context
* @return {ObjectExpression | ArrayExpression}}
*/
export function build_style_directives_object(style_directives, context) {
export function build_style_directives_object(style_directives, expressions, context) {
let normal_properties = [];
let important_properties = [];
@ -542,7 +487,7 @@ export function build_style_directives_object(style_directives, context) {
directive.value === true
? build_getter({ name: directive.name, type: 'Identifier' }, context.state)
: build_attribute_value(directive.value, context, (value, metadata) =>
metadata.has_call ? get_expression_id(context.state, value) : value
metadata.has_call ? get_expression_id(expressions, value) : value
).value;
const property = b.init(directive.name, expression);
@ -560,7 +505,7 @@ export function build_style_directives_object(style_directives, context) {
/**
* Serializes an assignment to an element property by adding relevant statements to either only
* the init or the the init and update arrays, depending on whether or not the value is dynamic.
* the init or the init and update arrays, depending on whether or not the value is dynamic.
* Resulting code for static looks something like this:
* ```js
* element.property = value;
@ -681,7 +626,7 @@ function build_element_special_value_attribute(element, node_id, attribute, cont
? // if is a select with value we will also invoke `init_select` which need a reference before the template effect so we memoize separately
is_select_with_value
? memoize_expression(state, value)
: get_expression_id(state, value)
: get_expression_id(state.expressions, value)
: value
);

@ -9,7 +9,7 @@ import * as b from '#compiler/builders';
* @param {ComponentContext} context
*/
export function RenderTag(node, context) {
context.state.template.push('<!>');
context.state.template.push_comment();
const expression = unwrap_optional(node.expression);

@ -11,7 +11,7 @@ import { memoize_expression } from './shared/utils.js';
*/
export function SlotElement(node, context) {
// <slot {a}>fallback</slot> --> $.slot($$slots.default, { get a() { .. } }, () => ...fallback);
context.state.template.push('<!>');
context.state.template.push_comment();
/** @type {Property[]} */
const props = [];

@ -43,14 +43,21 @@ export function SnippetBlock(node, context) {
let arg_alias = `$$arg${i}`;
args.push(b.id(arg_alias));
const paths = extract_paths(argument);
const { inserts, paths } = extract_paths(argument, b.maybe_call(b.id(arg_alias)));
for (const { id, value } of inserts) {
id.name = context.state.scope.generate('$$array');
transform[id.name] = { read: get_value };
declarations.push(
b.var(id, b.call('$.derived', /** @type {Expression} */ (context.visit(b.thunk(value)))))
);
}
for (const path of paths) {
const name = /** @type {Identifier} */ (path.node).name;
const needs_derived = path.has_default_value; // to ensure that default value is only called once
const fn = b.thunk(
/** @type {Expression} */ (context.visit(path.expression?.(b.maybe_call(b.id(arg_alias)))))
);
const fn = b.thunk(/** @type {Expression} */ (context.visit(path.expression, child_state)));
declarations.push(b.let(path.node, needs_derived ? b.call('$.derived_safe_equal', fn) : fn));

@ -1,4 +1,4 @@
/** @import { BlockStatement, Statement, Expression } from 'estree' */
/** @import { BlockStatement, Statement, Expression, FunctionDeclaration, VariableDeclaration, ArrowFunctionExpression } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */
import { dev } from '../../../../state.js';
@ -34,62 +34,61 @@ export function SvelteBoundary(node, context) {
const nodes = [];
/** @type {Statement[]} */
const external_statements = [];
const const_tags = [];
/** @type {Statement[]} */
const internal_statements = [];
const hoisted = [];
const snippets_visits = [];
// const tags need to live inside the boundary, but might also be referenced in hoisted snippets.
// to resolve this we cheat: we duplicate const tags inside snippets
for (const child of node.fragment.nodes) {
if (child.type === 'ConstTag') {
context.visit(child, { ...context.state, init: const_tags });
}
}
// Capture the `failed` implicit snippet prop
for (const child of node.fragment.nodes) {
if (child.type === 'SnippetBlock' && child.expression.name === 'failed') {
// we need to delay the visit of the snippets in case they access a ConstTag that is declared
// after the snippets so that the visitor for the const tag can be updated
snippets_visits.push(() => {
if (child.type === 'ConstTag') {
continue;
}
if (child.type === 'SnippetBlock') {
/** @type {Statement[]} */
const init = [];
context.visit(child, { ...context.state, init });
const statements = [];
context.visit(child, { ...context.state, init: statements });
const snippet = /** @type {VariableDeclaration} */ (statements[0]);
const snippet_fn = dev
? // @ts-expect-error we know this shape is correct
snippet.declarations[0].init.arguments[1]
: snippet.declarations[0].init;
snippet_fn.body.body.unshift(
...const_tags.filter((node) => node.type === 'VariableDeclaration')
);
hoisted.push(snippet);
if (child.expression.name === 'failed') {
props.properties.push(b.prop('init', child.expression, child.expression));
external_statements.push(...init);
});
} else if (child.type === 'ConstTag') {
/** @type {Statement[]} */
const init = [];
context.visit(child, { ...context.state, init });
if (dev) {
// In dev we must separate the declarations from the code
// that eagerly evaluate the expression...
for (const statement of init) {
if (statement.type === 'VariableDeclaration') {
external_statements.push(statement);
} else {
internal_statements.push(statement);
}
}
} else {
external_statements.push(...init);
continue;
}
} else {
nodes.push(child);
}
}
snippets_visits.forEach((visit) => visit());
const block = /** @type {BlockStatement} */ (context.visit({ ...node.fragment, nodes }));
if (dev && internal_statements.length) {
block.body.unshift(...internal_statements);
}
block.body.unshift(...const_tags);
const boundary = b.stmt(
b.call('$.boundary', context.state.node, props, b.arrow([b.id('$$anchor')], block))
);
context.state.template.push('<!>');
context.state.init.push(
external_statements.length > 0 ? b.block([...external_statements, boundary]) : boundary
);
context.state.template.push_comment();
context.state.init.push(hoisted.length > 0 ? b.block([...hoisted, boundary]) : boundary);
}

@ -5,7 +5,11 @@ import { dev, locator } from '../../../../state.js';
import { is_text_attribute } from '../../../../utils/ast.js';
import * as b from '#compiler/builders';
import { determine_namespace_for_children } from '../../utils.js';
import { build_attribute_value, build_set_attributes, build_set_class } from './shared/element.js';
import {
build_attribute_value,
build_attribute_effect,
build_set_class
} from './shared/element.js';
import { build_render_statement } from './shared/utils.js';
/**
@ -13,7 +17,7 @@ import { build_render_statement } from './shared/utils.js';
* @param {ComponentContext} context
*/
export function SvelteElement(node, context) {
context.state.template.push(`<!>`);
context.state.template.push_comment();
/** @type {Array<AST.Attribute | AST.SpreadAttribute>} */
const attributes = [];
@ -80,18 +84,15 @@ export function SvelteElement(node, context) {
) {
build_set_class(node, element_id, attributes[0], class_directives, inner_context, false);
} else if (attributes.length) {
const attributes_id = b.id(context.state.scope.generate('attributes'));
// Always use spread because we don't know whether the element is a custom element or not,
// therefore we need to do the "how to set an attribute" logic at runtime.
build_set_attributes(
build_attribute_effect(
attributes,
class_directives,
style_directives,
inner_context,
node,
element_id,
attributes_id
element_id
);
}

@ -15,7 +15,7 @@ export function UpdateExpression(node, context) {
argument.type === 'MemberExpression' &&
argument.object.type === 'ThisExpression' &&
argument.property.type === 'PrivateIdentifier' &&
context.state.private_state.has(argument.property.name)
context.state.state_fields.has('#' + argument.property.name)
) {
let fn = '$.update';
if (node.prefix) fn += '_pre';

@ -8,6 +8,7 @@ import * as assert from '../../../../utils/assert.js';
import { get_rune } from '../../../scope.js';
import { get_prop_source, is_prop_source, is_state_source, should_proxy } from '../utils.js';
import { is_hoisted_function } from '../../utils.js';
import { get_value } from './shared/declarations.js';
/**
* @param {VariableDeclaration} node
@ -89,6 +90,10 @@ export function VariableDeclaration(node, context) {
should_proxy(initial, context.state.scope)
) {
initial = b.call('$.proxy', initial);
if (dev) {
initial = b.call('$.tag_proxy', initial, b.literal(id.name));
}
}
if (is_prop_source(binding, context.state)) {
@ -116,7 +121,7 @@ export function VariableDeclaration(node, context) {
}
const args = /** @type {CallExpression} */ (init).arguments;
const value = args.length > 0 ? /** @type {Expression} */ (context.visit(args[0])) : b.void0;
const value = /** @type {Expression} */ (args[0]) ?? b.void0; // TODO do we need the void 0? can we just omit it altogether?
if (rune === '$state' || rune === '$state.raw') {
/**
@ -127,26 +132,53 @@ export function VariableDeclaration(node, context) {
const binding = /** @type {import('#compiler').Binding} */ (
context.state.scope.get(id.name)
);
if (rune === '$state' && should_proxy(value, context.state.scope)) {
const is_state = is_state_source(binding, context.state.analysis);
const is_proxy = should_proxy(value, context.state.scope);
if (rune === '$state' && is_proxy) {
value = b.call('$.proxy', value);
if (dev && !is_state) {
value = b.call('$.tag_proxy', value, b.literal(id.name));
}
if (is_state_source(binding, context.state.analysis)) {
}
if (is_state) {
value = b.call('$.state', value);
if (dev) {
value = b.call('$.tag', value, b.literal(id.name));
}
}
return value;
};
if (declarator.id.type === 'Identifier') {
const expression = /** @type {Expression} */ (context.visit(value));
declarations.push(
b.declarator(declarator.id, create_state_declarator(declarator.id, value))
b.declarator(declarator.id, create_state_declarator(declarator.id, expression))
);
} else {
const tmp = context.state.scope.generate('tmp');
const paths = extract_paths(declarator.id);
const tmp = b.id(context.state.scope.generate('tmp'));
const { inserts, paths } = extract_paths(declarator.id, tmp);
declarations.push(
b.declarator(b.id(tmp), value),
b.declarator(tmp, /** @type {Expression} */ (context.visit(value))),
...inserts.map(({ id, value }) => {
id.name = context.state.scope.generate('$$array');
context.state.transform[id.name] = { read: get_value };
const expression = /** @type {Expression} */ (context.visit(b.thunk(value)));
const call = b.call('$.derived', expression);
return b.declarator(
id,
dev ? b.call('$.tag', call, b.literal('[$state iterable]')) : call
);
}),
...paths.map((path) => {
const value = path.expression?.(b.id(tmp));
const value = /** @type {Expression} */ (context.visit(path.expression));
const binding = context.state.scope.get(/** @type {Identifier} */ (path.node).name);
return b.declarator(
path.node,
@ -163,39 +195,59 @@ export function VariableDeclaration(node, context) {
if (rune === '$derived' || rune === '$derived.by') {
if (declarator.id.type === 'Identifier') {
let expression = /** @type {Expression} */ (context.visit(value));
if (rune === '$derived') expression = b.thunk(expression);
const call = b.call('$.derived', expression);
declarations.push(
b.declarator(
declarator.id,
b.call('$.derived', rune === '$derived.by' ? value : b.thunk(value))
dev ? b.call('$.tag', call, b.literal(declarator.id.name)) : call
)
);
} else {
const bindings = extract_paths(declarator.id);
const init = /** @type {CallExpression} */ (declarator.init);
/** @type {Identifier} */
let id;
let rhs = value;
if (rune === '$derived' && init.arguments[0].type === 'Identifier') {
id = init.arguments[0];
} else {
id = b.id(context.state.scope.generate('$$d'));
if (rune !== '$derived' || init.arguments[0].type !== 'Identifier') {
const id = b.id(context.state.scope.generate('$$d'));
rhs = b.call('$.get', id);
let expression = /** @type {Expression} */ (context.visit(value));
if (rune === '$derived') expression = b.thunk(expression);
const call = b.call('$.derived', expression);
declarations.push(
b.declarator(id, dev ? b.call('$.tag', call, b.literal('[$derived iterable]')) : call)
);
}
const { inserts, paths } = extract_paths(declarator.id, rhs);
for (const { id, value } of inserts) {
id.name = context.state.scope.generate('$$array');
context.state.transform[id.name] = { read: get_value };
const expression = /** @type {Expression} */ (context.visit(b.thunk(value)));
const call = b.call('$.derived', expression);
declarations.push(
b.declarator(id, b.call('$.derived', rune === '$derived.by' ? value : b.thunk(value)))
b.declarator(id, dev ? b.call('$.tag', call, b.literal('[$derived iterable]')) : call)
);
}
for (let i = 0; i < bindings.length; i++) {
const binding = bindings[i];
for (const path of paths) {
const expression = /** @type {Expression} */ (context.visit(path.expression));
const call = b.call('$.derived', b.thunk(expression));
declarations.push(
b.declarator(binding.node, b.call('$.derived', b.thunk(binding.expression(rhs))))
b.declarator(
path.node,
dev
? b.call('$.tag', call, b.literal(/** @type {Identifier} */ (path.node).name))
: call
)
);
}
}
continue;
}
}
@ -224,20 +276,29 @@ export function VariableDeclaration(node, context) {
if (declarator.id.type !== 'Identifier') {
// Turn export let into props. It's really really weird because export let { x: foo, z: [bar]} = ..
// means that foo and bar are the props (i.e. the leafs are the prop names), not x and z.
const tmp = context.state.scope.generate('tmp');
const paths = extract_paths(declarator.id);
const tmp = b.id(context.state.scope.generate('tmp'));
const { inserts, paths } = extract_paths(declarator.id, tmp);
declarations.push(
b.declarator(
b.id(tmp),
tmp,
/** @type {Expression} */ (context.visit(/** @type {Expression} */ (declarator.init)))
)
);
for (const { id, value } of inserts) {
id.name = context.state.scope.generate('$$array');
context.state.transform[id.name] = { read: get_value };
const expression = /** @type {Expression} */ (context.visit(b.thunk(value)));
declarations.push(b.declarator(id, b.call('$.derived', expression)));
}
for (const path of paths) {
const name = /** @type {Identifier} */ (path.node).name;
const binding = /** @type {Binding} */ (context.state.scope.get(name));
const value = path.expression?.(b.id(tmp));
const value = /** @type {Expression} */ (context.visit(path.expression));
declarations.push(
b.declarator(
path.node,
@ -271,7 +332,7 @@ export function VariableDeclaration(node, context) {
declarations.push(
...create_state_declarators(
declarator,
context.state,
context,
/** @type {Expression} */ (declarator.init && context.visit(declarator.init))
)
);
@ -291,30 +352,39 @@ export function VariableDeclaration(node, context) {
/**
* Creates the output for a state declaration in legacy mode.
* @param {VariableDeclarator} declarator
* @param {ComponentClientTransformState} scope
* @param {ComponentContext} context
* @param {Expression} value
*/
function create_state_declarators(declarator, { scope, analysis }, value) {
function create_state_declarators(declarator, context, value) {
if (declarator.id.type === 'Identifier') {
return [
b.declarator(
declarator.id,
b.call('$.mutable_source', value, analysis.immutable ? b.true : undefined)
b.call('$.mutable_source', value, context.state.analysis.immutable ? b.true : undefined)
)
];
}
const tmp = scope.generate('tmp');
const paths = extract_paths(declarator.id);
const tmp = b.id(context.state.scope.generate('tmp'));
const { inserts, paths } = extract_paths(declarator.id, tmp);
return [
b.declarator(b.id(tmp), value),
b.declarator(tmp, value),
...inserts.map(({ id, value }) => {
id.name = context.state.scope.generate('$$array');
context.state.transform[id.name] = { read: get_value };
const expression = /** @type {Expression} */ (context.visit(b.thunk(value)));
return b.declarator(id, b.call('$.derived', expression));
}),
...paths.map((path) => {
const value = path.expression?.(b.id(tmp));
const binding = scope.get(/** @type {Identifier} */ (path.node).name);
const value = /** @type {Expression} */ (context.visit(path.expression));
const binding = context.state.scope.get(/** @type {Identifier} */ (path.node).name);
return b.declarator(
path.node,
binding?.kind === 'state'
? b.call('$.mutable_source', value, analysis.immutable ? b.true : undefined)
? b.call('$.mutable_source', value, context.state.analysis.immutable ? b.true : undefined)
: value
);
})

@ -13,10 +13,13 @@ import { determine_slot } from '../../../../../utils/slot.js';
* @param {AST.Component | AST.SvelteComponent | AST.SvelteSelf} node
* @param {string} component_name
* @param {ComponentContext} context
* @param {Expression} anchor
* @returns {Statement}
*/
export function build_component(node, component_name, context, anchor = context.state.node) {
export function build_component(node, component_name, context) {
/**
* @type {Expression}
*/
const anchor = context.state.node;
/** @type {Array<Property[] | Expression>} */
const props_and_spreads = [];
/** @type {Array<() => void>} */
@ -257,6 +260,22 @@ export function build_component(node, component_name, context, anchor = context.
);
}
}
} else if (attribute.type === 'AttachTag') {
const evaluated = context.state.scope.evaluate(attribute.expression);
let expression = /** @type {Expression} */ (context.visit(attribute.expression));
if (attribute.metadata.expression.has_state) {
expression = b.arrow(
[b.id('$$node')],
b.call(
evaluated.is_function ? expression : b.logical('||', expression, b.id('$.noop')),
b.id('$$node')
)
);
}
push_prop(b.prop('init', b.call('$.attachment'), expression, true));
}
}
@ -395,7 +414,7 @@ export function build_component(node, component_name, context, anchor = context.
// TODO We can remove this ternary once we remove legacy mode, since in runes mode dynamic components
// will be handled separately through the `$.component` function, and then the component name will
// always be referenced through just the identifier here.
node.type === 'SvelteComponent'
node.type === 'SvelteComponent' || (node.type === 'Component' && node.metadata.dynamic)
? component_name
: /** @type {Expression} */ (context.visit(b.member_id(component_name))),
node_id,
@ -413,14 +432,18 @@ export function build_component(node, component_name, context, anchor = context.
const statements = [...snippet_declarations];
if (node.type === 'SvelteComponent') {
if (node.type === 'SvelteComponent' || (node.type === 'Component' && node.metadata.dynamic)) {
const prev = fn;
fn = (node_id) => {
return b.call(
'$.component',
node_id,
b.thunk(/** @type {Expression} */ (context.visit(node.expression))),
b.thunk(
/** @type {Expression} */ (
context.visit(node.type === 'Component' ? b.member_id(node.name) : node.expression)
)
),
b.arrow(
[b.id('$$anchor'), b.id(component_name)],
b.block([...binding_initializers, b.stmt(prev(b.id('$$anchor')))])
@ -432,11 +455,17 @@ export function build_component(node, component_name, context, anchor = context.
}
if (Object.keys(custom_css_props).length > 0) {
context.state.template.push(
context.state.metadata.namespace === 'svg'
? '<g><!></g>'
: '<svelte-css-wrapper style="display: contents"><!></svelte-css-wrapper>'
);
if (context.state.metadata.namespace === 'svg') {
// this boils down to <g><!></g>
context.state.template.push_element('g', node.start);
} else {
// this boils down to <svelte-css-wrapper style='display: contents'><!></svelte-css-wrapper>
context.state.template.push_element('svelte-css-wrapper', node.start);
context.state.template.set_prop('style', 'display: contents');
}
context.state.template.push_comment();
context.state.template.pop_element();
statements.push(
b.stmt(b.call('$.css_props', anchor, b.thunk(b.object(custom_css_props)))),
@ -444,7 +473,7 @@ export function build_component(node, component_name, context, anchor = context.
b.stmt(b.call('$.reset', anchor))
);
} else {
context.state.template.push('<!>');
context.state.template.push_comment();
statements.push(b.stmt(fn(anchor)));
}

@ -16,28 +16,30 @@ import { build_template_chunk, get_expression_id } from './utils.js';
* @param {ComponentContext} context
* @param {AST.RegularElement | AST.SvelteElement} element
* @param {Identifier} element_id
* @param {Identifier} attributes_id
*/
export function build_set_attributes(
export function build_attribute_effect(
attributes,
class_directives,
style_directives,
context,
element,
element_id,
attributes_id
element_id
) {
let is_dynamic = false;
/** @type {ObjectExpression['properties']} */
const values = [];
/** @type {Expression[]} */
const expressions = [];
/** @param {Expression} value */
function memoize(value) {
return b.id(`$${expressions.push(value) - 1}`);
}
for (const attribute of attributes) {
if (attribute.type === 'Attribute') {
const { value, has_state } = build_attribute_value(
attribute.value,
context,
(value, metadata) => (metadata.has_call ? get_expression_id(context.state, value) : value)
const { value } = build_attribute_value(attribute.value, context, (value, metadata) =>
metadata.has_call ? memoize(value) : value
);
if (
@ -51,16 +53,11 @@ export function build_set_attributes(
} else {
values.push(b.init(attribute.name, value));
}
is_dynamic ||= has_state;
} else {
// objects could contain reactive getters -> play it safe and always assume spread attributes are reactive
is_dynamic = true;
let value = /** @type {Expression} */ (context.visit(attribute));
if (attribute.metadata.expression.has_call) {
value = get_expression_id(context.state, value);
value = memoize(value);
}
values.push(b.spread(value));
@ -72,12 +69,9 @@ export function build_set_attributes(
b.prop(
'init',
b.array([b.id('$.CLASS')]),
build_class_directives_object(class_directives, context)
build_class_directives_object(class_directives, expressions, context)
)
);
is_dynamic ||=
class_directives.find((directive) => directive.metadata.expression.has_state) !== null;
}
if (style_directives.length) {
@ -85,31 +79,28 @@ export function build_set_attributes(
b.prop(
'init',
b.array([b.id('$.STYLE')]),
build_style_directives_object(style_directives, context)
build_style_directives_object(style_directives, expressions, context)
)
);
is_dynamic ||= style_directives.some((directive) => directive.metadata.expression.has_state);
}
const call = b.call(
'$.set_attributes',
context.state.init.push(
b.stmt(
b.call(
'$.attribute_effect',
element_id,
is_dynamic ? attributes_id : b.null,
b.object(values),
b.arrow(
expressions.map((_, i) => b.id(`$${i}`)),
b.object(values)
),
expressions.length > 0 && b.array(expressions.map((expression) => b.thunk(expression))),
element.metadata.scoped &&
context.state.analysis.css.hash !== '' &&
b.literal(context.state.analysis.css.hash),
is_ignored(element, 'hydration_attribute_changed') && b.true
)
)
);
if (is_dynamic) {
context.state.init.push(b.let(attributes_id));
const update = b.stmt(b.assignment('=', attributes_id, call));
context.state.update.push(update);
} else {
context.state.init.push(b.stmt(call));
}
}
/**
@ -167,7 +158,7 @@ export function build_set_class(element, node_id, attribute, class_directives, c
value = b.call('$.clsx', value);
}
return metadata.has_call ? get_expression_id(context.state, value) : value;
return metadata.has_call ? get_expression_id(context.state.expressions, value) : value;
});
/** @type {Identifier | undefined} */
@ -180,7 +171,7 @@ export function build_set_class(element, node_id, attribute, class_directives, c
let next;
if (class_directives.length) {
next = build_class_directives_object(class_directives, context);
next = build_class_directives_object(class_directives, context.state.expressions, context);
has_state ||= class_directives.some((d) => d.metadata.expression.has_state);
if (has_state) {
@ -235,7 +226,7 @@ export function build_set_class(element, node_id, attribute, class_directives, c
*/
export function build_set_style(node_id, attribute, style_directives, context) {
let { value, has_state } = build_attribute_value(attribute.value, context, (value, metadata) =>
metadata.has_call ? get_expression_id(context.state, value) : value
metadata.has_call ? get_expression_id(context.state.expressions, value) : value
);
/** @type {Identifier | undefined} */
@ -248,7 +239,7 @@ export function build_set_style(node_id, attribute, style_directives, context) {
let next;
if (style_directives.length) {
next = build_style_directives_object(style_directives, context);
next = build_style_directives_object(style_directives, context.state.expressions, context);
has_state ||= style_directives.some((d) => d.metadata.expression.has_state);
if (has_state) {

@ -64,11 +64,11 @@ export function process_children(nodes, initial, is_element, { visit, state }) {
function flush_sequence(sequence) {
if (sequence.every((node) => node.type === 'Text')) {
skipped += 1;
state.template.push(sequence.map((node) => node.raw).join(''));
state.template.push_text(sequence);
return;
}
state.template.push(' ');
state.template.push_text([{ type: 'Text', data: ' ', raw: ' ', start: -1, end: -1 }]);
const { has_state, value } = build_template_chunk(sequence, visit, state);

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

Loading…
Cancel
Save