diff --git a/.changeset/strong-coins-peel.md b/.changeset/strong-coins-peel.md deleted file mode 100644 index 013e8e44a1..0000000000 --- a/.changeset/strong-coins-peel.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: improve error message for migration errors when slot would be renamed diff --git a/.changeset/wild-bulldogs-move.md b/.changeset/wild-bulldogs-move.md deleted file mode 100644 index c3c5580f77..0000000000 --- a/.changeset/wild-bulldogs-move.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: allow characters in the supplementary special-purpose plane diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cf73a1f6cb..c0e1d36760 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 diff --git a/benchmarking/compare/index.js b/benchmarking/compare/index.js index 9d8d279c35..8f38686a29 100644 --- a/benchmarking/compare/index.js +++ b/benchmarking/compare/index.js @@ -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); +} diff --git a/documentation/docs/02-runes/02-$state.md b/documentation/docs/02-runes/02-$state.md index 16630a977b..8e6c91fad7 100644 --- a/documentation/docs/02-runes/02-$state.md +++ b/documentation/docs/02-runes/02-$state.md @@ -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`: diff --git a/documentation/docs/02-runes/04-$effect.md b/documentation/docs/02-runes/04-$effect.md index 46ea9b81e9..0e129973d5 100644 --- a/documentation/docs/02-runes/04-$effect.md +++ b/documentation/docs/02-runes/04-$effect.md @@ -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 ``` diff --git a/documentation/docs/03-template-syntax/01-basic-markup.md b/documentation/docs/03-template-syntax/01-basic-markup.md index fe5f8b02aa..feecfe033e 100644 --- a/documentation/docs/03-template-syntax/01-basic-markup.md +++ b/documentation/docs/03-template-syntax/01-basic-markup.md @@ -82,12 +82,14 @@ As with elements, `name={name}` can be replaced with the `{name}` shorthand. ``` +## 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 - + ``` ## Events diff --git a/documentation/docs/03-template-syntax/03-each.md b/documentation/docs/03-template-syntax/03-each.md index 70666f6a57..006cadd152 100644 --- a/documentation/docs/03-template-syntax/03-each.md +++ b/documentation/docs/03-template-syntax/03-each.md @@ -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)} diff --git a/documentation/docs/03-template-syntax/09-@attach.md b/documentation/docs/03-template-syntax/09-@attach.md new file mode 100644 index 0000000000..b25fbb32a6 --- /dev/null +++ b/documentation/docs/03-template-syntax/09-@attach.md @@ -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 + + + +
...
+``` + +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 + + + + + + +``` + +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 + + { + const context = canvas.getContext('2d'); + + $effect(() => { + context.fillStyle = color; + context.fillRect(0, 0, canvas.width, canvas.height); + }); + }} +> +``` + +> [!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 + + + + + +``` + +```svelte + + + + + + +``` + +## 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. diff --git a/documentation/docs/03-template-syntax/09-@const.md b/documentation/docs/03-template-syntax/10-@const.md similarity index 100% rename from documentation/docs/03-template-syntax/09-@const.md rename to documentation/docs/03-template-syntax/10-@const.md diff --git a/documentation/docs/03-template-syntax/10-@debug.md b/documentation/docs/03-template-syntax/11-@debug.md similarity index 100% rename from documentation/docs/03-template-syntax/10-@debug.md rename to documentation/docs/03-template-syntax/11-@debug.md diff --git a/documentation/docs/03-template-syntax/11-bind.md b/documentation/docs/03-template-syntax/12-bind.md similarity index 80% rename from documentation/docs/03-template-syntax/11-bind.md rename to documentation/docs/03-template-syntax/12-bind.md index c23f3b5232..de57815687 100644 --- a/documentation/docs/03-template-syntax/11-bind.md +++ b/documentation/docs/03-template-syntax/12-bind.md @@ -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: ```svelte @@ -117,28 +117,52 @@ Since 5.6.0, if an `` has a `defaultChecked` attribute and is part of a f ``` +## `` + +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 + + +
+ + + {#if indeterminate} + waiting... + {:else if checked} + checked + {:else} + unchecked + {/if} +
+``` + ## `` -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 + - - - + + + - - - - + + + + ``` > [!NOTE] `bind:group` only works if the inputs are in the same Svelte component. @@ -227,6 +251,7 @@ You can give the `` a default value by adding a `selected` attribute to ``` +## `window` and `document` + +To bind to properties of `window` and `document`, see [``](svelte-window) and [``](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
@@ -285,7 +318,7 @@ All visible elements have the following readonly bindings, measured with a `Resi
``` -> [!NOTE] `display: inline` elements do not have a width or height (except for elements with 'intrinsic' dimensions, like `` and ``), 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 `` and ``), 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 diff --git a/documentation/docs/03-template-syntax/12-use.md b/documentation/docs/03-template-syntax/13-use.md similarity index 93% rename from documentation/docs/03-template-syntax/12-use.md rename to documentation/docs/03-template-syntax/13-use.md index 45de023578..5f5321a1c0 100644 --- a/documentation/docs/03-template-syntax/12-use.md +++ b/documentation/docs/03-template-syntax/13-use.md @@ -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 diff --git a/documentation/docs/03-template-syntax/13-transition.md b/documentation/docs/03-template-syntax/14-transition.md similarity index 100% rename from documentation/docs/03-template-syntax/13-transition.md rename to documentation/docs/03-template-syntax/14-transition.md diff --git a/documentation/docs/03-template-syntax/14-in-and-out.md b/documentation/docs/03-template-syntax/15-in-and-out.md similarity index 100% rename from documentation/docs/03-template-syntax/14-in-and-out.md rename to documentation/docs/03-template-syntax/15-in-and-out.md diff --git a/documentation/docs/03-template-syntax/15-animate.md b/documentation/docs/03-template-syntax/16-animate.md similarity index 100% rename from documentation/docs/03-template-syntax/15-animate.md rename to documentation/docs/03-template-syntax/16-animate.md diff --git a/documentation/docs/03-template-syntax/17-style.md b/documentation/docs/03-template-syntax/17-style.md index 749376c6e2..aa61cdcde3 100644 --- a/documentation/docs/03-template-syntax/17-style.md +++ b/documentation/docs/03-template-syntax/17-style.md @@ -34,8 +34,10 @@ To mark a style as important, use the `|important` modifier:
...
``` -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 -
This will be red
+
This will be red
+
This will still be red
``` diff --git a/documentation/docs/05-special-elements/04-svelte-body.md b/documentation/docs/05-special-elements/04-svelte-body.md index d6536b0b74..c6828b98f7 100644 --- a/documentation/docs/05-special-elements/04-svelte-body.md +++ b/documentation/docs/05-special-elements/04-svelte-body.md @@ -8,7 +8,7 @@ title: Similarly to ``, 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 `` element. -As with `` and ``, this element may only appear the top level of your component and must never be inside a block or element. +As with `` and ``, this element may only appear at the top level of your component and must never be inside a block or element. ```svelte diff --git a/documentation/docs/06-runtime/02-context.md b/documentation/docs/06-runtime/02-context.md index 4204bcfe6d..f395de421c 100644 --- a/documentation/docs/06-runtime/02-context.md +++ b/documentation/docs/06-runtime/02-context.md @@ -125,7 +125,7 @@ In many cases this is perfectly fine, but there is a risk: if you mutate the sta ```svelte + * + * + * ``` + * @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 + * + *
...
+ * + * + *
bar)}>...
+ * ``` + * @template {EventTarget} E + * @template {unknown} T + * @overload + * @param {Action | ((element: E, arg: T) => void | ActionReturn)} action The action function + * @param {() => T} fn A function that returns the argument for the action + * @returns {Attachment} + */ +/** + * 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 + * + *
...
+ * + * + *
bar)}>...
+ * ``` + * @template {EventTarget} E + * @overload + * @param {Action | ((element: E) => void | ActionReturn)} action The action function + * @returns {Attachment} + */ +/** + * 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 + * + *
...
+ * + * + *
bar)}>...
+ * ``` + * + * @template {EventTarget} E + * @template {unknown} T + * @param {Action | ((element: E, arg: T) => void | ActionReturn)} action The action function + * @param {() => T} fn A function that returns the argument for the action + * @returns {Attachment} + * @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); + } + }; +} diff --git a/packages/svelte/src/attachments/public.d.ts b/packages/svelte/src/attachments/public.d.ts new file mode 100644 index 0000000000..caf1342d0a --- /dev/null +++ b/packages/svelte/src/attachments/public.d.ts @@ -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 { + (element: T): void | (() => void); +} + +export * from './index.js'; diff --git a/packages/svelte/src/compiler/errors.js b/packages/svelte/src/compiler/errors.js index c99f597468..25e72340c6 100644 --- a/packages/svelte/src/compiler/errors.js +++ b/packages/svelte/src/compiler/errors.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`); } /** diff --git a/packages/svelte/src/compiler/index.js b/packages/svelte/src/compiler/index.js index 42427dd9c4..756a88a824 100644 --- a/packages/svelte/src/compiler/index.js +++ b/packages/svelte/src/compiler/index.js @@ -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); diff --git a/packages/svelte/src/compiler/legacy.js b/packages/svelte/src/compiler/legacy.js index e3f88c8f1d..f6b7e4b054 100644 --- a/packages/svelte/src/compiler/legacy.js +++ b/packages/svelte/src/compiler/legacy.js @@ -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 diff --git a/packages/svelte/src/compiler/migrate/index.js b/packages/svelte/src/compiler/migrate/index.js index 2d5a4dcd9e..5ca9adb98b 100644 --- a/packages/svelte/src/compiler/migrate/index.js +++ b/packages/svelte/src/compiler/migrate/index.js @@ -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, diff --git a/packages/svelte/src/compiler/phases/1-parse/acorn.js b/packages/svelte/src/compiler/phases/1-parse/acorn.js index 36f7688c49..26a09abb66 100644 --- a/packages/svelte/src/compiler/phases/1-parse/acorn.js +++ b/packages/svelte/src/compiler/phases/1-parse/acorn.js @@ -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 }); diff --git a/packages/svelte/src/compiler/phases/1-parse/read/context.js b/packages/svelte/src/compiler/phases/1-parse/read/context.js index f4c73dcf40..b118901830 100644 --- a/packages/svelte/src/compiler/phases/1-parse/read/context.js +++ b/packages/svelte/src/compiler/phases/1-parse/read/context.js @@ -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} diff --git a/packages/svelte/src/compiler/phases/1-parse/read/script.js b/packages/svelte/src/compiler/phases/1-parse/read/script.js index 9d9ed3a1ef..6290127811 100644 --- a/packages/svelte/src/compiler/phases/1-parse/read/script.js +++ b/packages/svelte/src/compiler/phases/1-parse/read/script.js @@ -16,7 +16,7 @@ const ALLOWED_ATTRIBUTES = ['context', 'generics', 'lang', 'module']; /** * @param {Parser} parser * @param {number} start - * @param {Array} attributes + * @param {Array} attributes * @returns {AST.Script} */ export function read_script(parser, start, attributes) { diff --git a/packages/svelte/src/compiler/phases/1-parse/read/style.js b/packages/svelte/src/compiler/phases/1-parse/read/style.js index 56dbe124b7..80ab234d92 100644 --- a/packages/svelte/src/compiler/phases/1-parse/read/style.js +++ b/packages/svelte/src/compiler/phases/1-parse/read/style.js @@ -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} attributes + * @param {Array} 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 === '') { diff --git a/packages/svelte/src/compiler/phases/1-parse/remove_typescript_nodes.js b/packages/svelte/src/compiler/phases/1-parse/remove_typescript_nodes.js index aba94ee20d..cb498c3c13 100644 --- a/packages/svelte/src/compiler/phases/1-parse/remove_typescript_nodes.js +++ b/packages/svelte/src/compiler/phases/1-parse/remove_typescript_nodes.js @@ -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; diff --git a/packages/svelte/src/compiler/phases/1-parse/state/element.js b/packages/svelte/src/compiler/phases/1-parse/state/element.js index 66946a8f8d..6b6c9160d8 100644 --- a/packages/svelte/src/compiler/phases/1-parse/state/element.js +++ b/packages/svelte/src/compiler/phases/1-parse/state/element.js @@ -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 }, + ``, + `` + ); + } + } 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.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); diff --git a/packages/svelte/src/compiler/phases/1-parse/state/tag.js b/packages/svelte/src/compiler/phases/1-parse/state/tag.js index 0eb98c27e8..4153463c83 100644 --- a/packages/svelte/src/compiler/phases/1-parse/state/tag.js +++ b/packages/svelte/src/compiler/phases/1-parse/state/tag.js @@ -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(...)` + /** @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: { diff --git a/packages/svelte/src/compiler/phases/1-parse/utils/bracket.js b/packages/svelte/src/compiler/phases/1-parse/utils/bracket.js index b7c8cb43cd..8c69a58c99 100644 --- a/packages/svelte/src/compiler/phases/1-parse/utils/bracket.js +++ b/packages/svelte/src/compiler/phases/1-parse/utils/bracket.js @@ -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} */ +const default_brackets = { + '{': '}', + '(': ')', + '[': ']' +}; + +/** + * @param {Parser} parser + * @param {number} start + * @param {Record} 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); +} diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index 2e36a89649..fded183b86 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -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 }; diff --git a/packages/svelte/src/compiler/phases/2-analyze/types.d.ts b/packages/svelte/src/compiler/phases/2-analyze/types.d.ts index 17c8123de1..080239bac0 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/types.d.ts +++ b/packages/svelte/src/compiler/phases/2-analyze/types.d.ts @@ -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; /** 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; + function_depth: number; // legacy stuff diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/AssignmentExpression.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/AssignmentExpression.js index a64c89cd88..673c79f2df 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/AssignmentExpression.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/AssignmentExpression.js @@ -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; diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/AttachTag.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/AttachTag.js new file mode 100644 index 0000000000..1e318f228d --- /dev/null +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/AttachTag.js @@ -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 }); +} diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/Attribute.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/Attribute.js index 3ba81767cc..773aa59744 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/Attribute.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/Attribute.js @@ -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') || diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/BindDirective.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/BindDirective.js index 18ea79262b..9f02e7fa5a 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/BindDirective.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/BindDirective.js @@ -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); diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js index 904817b014..33abb52cac 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js @@ -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; +} diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/ClassBody.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/ClassBody.js index 0463e4da85..ffc39ac00d 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/ClassBody.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/ClassBody.js @@ -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} */ + 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 }); } diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/EachBlock.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/EachBlock.js index bd6c936f99..e6a83921b1 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/EachBlock.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/EachBlock.js @@ -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} 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); + } + } +} diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/Identifier.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/Identifier.js index efbbe6cfa2..abf70769c0 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/Identifier.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/Identifier.js @@ -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 ( diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/Literal.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/Literal.js new file mode 100644 index 0000000000..58684ba71c --- /dev/null +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/Literal.js @@ -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); + } + } +} diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/PropertyDefinition.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/PropertyDefinition.js new file mode 100644 index 0000000000..99d05cb47c --- /dev/null +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/PropertyDefinition.js @@ -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(); +} diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/TemplateElement.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/TemplateElement.js new file mode 100644 index 0000000000..978042bbc5 --- /dev/null +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/TemplateElement.js @@ -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); + } +} diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/Text.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/Text.js index 363a111b7d..a03421e8dd 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/Text.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/Text.js @@ -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 }); + } + } } diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/UpdateExpression.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/UpdateExpression.js index 741effc67a..13f4b9019e 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/UpdateExpression.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/UpdateExpression.js @@ -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; diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/VariableDeclarator.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/VariableDeclarator.js index a7d08d315d..89320f3962 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/VariableDeclarator.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/VariableDeclarator.js @@ -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') { diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/component.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/component.js index 04bf3d2ff3..aca87fab81 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/component.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/component.js @@ -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 — `` — @@ -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); + } + } +} diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/utils.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/utils.js index 12e21c386c..d7b682da08 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/utils.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/utils.js @@ -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; + } + } + } + } } /** diff --git a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js index f0da5a4918..e2e006c14b 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js @@ -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 }; diff --git a/packages/svelte/src/compiler/phases/3-transform/client/transform-template/fix-attribute-casing.js b/packages/svelte/src/compiler/phases/3-transform/client/transform-template/fix-attribute-casing.js new file mode 100644 index 0000000000..ce56c43d7c --- /dev/null +++ b/packages/svelte/src/compiler/phases/3-transform/client/transform-template/fix-attribute-casing.js @@ -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; +} diff --git a/packages/svelte/src/compiler/phases/3-transform/client/transform-template/index.js b/packages/svelte/src/compiler/phases/3-transform/client/transform-template/index.js new file mode 100644 index 0000000000..d0327e702a --- /dev/null +++ b/packages/svelte/src/compiler/phases/3-transform/client/transform-template/index.js @@ -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; +} diff --git a/packages/svelte/src/compiler/phases/3-transform/client/transform-template/template.js b/packages/svelte/src/compiler/phases/3-transform/client/transform-template/template.js new file mode 100644 index 0000000000..8f7f8a1f43 --- /dev/null +++ b/packages/svelte/src/compiler/phases/3-transform/client/transform-template/template.js @@ -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 ` + +

start

{#if cond}

cond

{/if} diff --git a/packages/svelte/tests/hydration/samples/cloudflare-mirage-borking/_config.js b/packages/svelte/tests/hydration/samples/cloudflare-mirage-borking/_config.js new file mode 100644 index 0000000000..56ba73b064 --- /dev/null +++ b/packages/svelte/tests/hydration/samples/cloudflare-mirage-borking/_config.js @@ -0,0 +1,6 @@ +import { test } from '../../test'; + +// https://github.com/sveltejs/svelte/issues/15819 +export default test({ + expect_hydration_error: true +}); diff --git a/packages/svelte/tests/hydration/samples/cloudflare-mirage-borking/_expected.html b/packages/svelte/tests/hydration/samples/cloudflare-mirage-borking/_expected.html new file mode 100644 index 0000000000..f6c03b87c1 --- /dev/null +++ b/packages/svelte/tests/hydration/samples/cloudflare-mirage-borking/_expected.html @@ -0,0 +1 @@ +

start

pre123 mid diff --git a/packages/svelte/tests/hydration/samples/cloudflare-mirage-borking/_override.html b/packages/svelte/tests/hydration/samples/cloudflare-mirage-borking/_override.html new file mode 100644 index 0000000000..c84efbb00b --- /dev/null +++ b/packages/svelte/tests/hydration/samples/cloudflare-mirage-borking/_override.html @@ -0,0 +1 @@ +

start

pre123 mid diff --git a/packages/svelte/tests/hydration/samples/cloudflare-mirage-borking/main.svelte b/packages/svelte/tests/hydration/samples/cloudflare-mirage-borking/main.svelte new file mode 100644 index 0000000000..2c9a94686e --- /dev/null +++ b/packages/svelte/tests/hydration/samples/cloudflare-mirage-borking/main.svelte @@ -0,0 +1,9 @@ + + +

start

+pre123 +{#if cond} +mid +{/if} diff --git a/packages/svelte/tests/hydration/test.ts b/packages/svelte/tests/hydration/test.ts index 266ac07bff..70d5c5d072 100644 --- a/packages/svelte/tests/hydration/test.ts +++ b/packages/svelte/tests/hydration/test.ts @@ -4,6 +4,7 @@ import * as fs from 'node:fs'; import { assert } from 'vitest'; import { compile_directory } from '../helpers.js'; import { assert_html_equal } from '../html_equal.js'; +import { fragments } from '../helpers.js'; import { assert_ok, suite, type BaseTest } from '../suite.js'; import { createClassComponent } from 'svelte/legacy'; import { render } from 'svelte/server'; @@ -43,7 +44,12 @@ function read(path: string): string | void { const { test, run } = suite(async (config, cwd) => { if (!config.load_compiled) { - await compile_directory(cwd, 'client', { accessors: true, ...config.compileOptions }); + await compile_directory(cwd, 'client', { + accessors: true, + fragments, + ...config.compileOptions + }); + await compile_directory(cwd, 'server', config.compileOptions); } @@ -125,7 +131,8 @@ const { test, run } = suite(async (config, cwd) => { flushSync(); - const normalize = (string: string) => string.trim().replace(/\r\n/g, '\n'); + const normalize = (string: string) => + string.trim().replaceAll('\r\n', '\n').replaceAll('/>', '>'); const expected = read(`${cwd}/_expected.html`) ?? rendered.html; assert.equal(normalize(target.innerHTML), normalize(expected)); diff --git a/packages/svelte/tests/parser-legacy/samples/action-duplicate/output.json b/packages/svelte/tests/parser-legacy/samples/action-duplicate/output.json index 3dad9bb4e5..c6af77a47b 100644 --- a/packages/svelte/tests/parser-legacy/samples/action-duplicate/output.json +++ b/packages/svelte/tests/parser-legacy/samples/action-duplicate/output.json @@ -15,16 +15,16 @@ "end": 20, "type": "Action", "name": "autofocus", - "modifiers": [], - "expression": null + "expression": null, + "modifiers": [] }, { "start": 21, "end": 34, "type": "Action", "name": "autofocus", - "modifiers": [], - "expression": null + "expression": null, + "modifiers": [] } ], "children": [] diff --git a/packages/svelte/tests/parser-legacy/samples/action-with-call/output.json b/packages/svelte/tests/parser-legacy/samples/action-with-call/output.json index 66ce187c62..a10d4eccf0 100644 --- a/packages/svelte/tests/parser-legacy/samples/action-with-call/output.json +++ b/packages/svelte/tests/parser-legacy/samples/action-with-call/output.json @@ -15,7 +15,6 @@ "end": 39, "type": "Action", "name": "tooltip", - "modifiers": [], "expression": { "type": "CallExpression", "start": 21, @@ -66,7 +65,8 @@ } ], "optional": false - } + }, + "modifiers": [] } ], "children": [] diff --git a/packages/svelte/tests/parser-legacy/samples/action-with-identifier/output.json b/packages/svelte/tests/parser-legacy/samples/action-with-identifier/output.json index 39a6f5f647..e9a3e7e5da 100644 --- a/packages/svelte/tests/parser-legacy/samples/action-with-identifier/output.json +++ b/packages/svelte/tests/parser-legacy/samples/action-with-identifier/output.json @@ -15,7 +15,6 @@ "end": 28, "type": "Action", "name": "tooltip", - "modifiers": [], "expression": { "type": "Identifier", "start": 20, @@ -31,7 +30,8 @@ } }, "name": "message" - } + }, + "modifiers": [] } ], "children": [] diff --git a/packages/svelte/tests/parser-legacy/samples/action-with-literal/output.json b/packages/svelte/tests/parser-legacy/samples/action-with-literal/output.json index 94c60b701a..94b60b9e5d 100644 --- a/packages/svelte/tests/parser-legacy/samples/action-with-literal/output.json +++ b/packages/svelte/tests/parser-legacy/samples/action-with-literal/output.json @@ -15,7 +15,6 @@ "end": 36, "type": "Action", "name": "tooltip", - "modifiers": [], "expression": { "type": "Literal", "start": 21, @@ -32,7 +31,8 @@ }, "value": "tooltip msg", "raw": "'tooltip msg'" - } + }, + "modifiers": [] } ], "children": [] diff --git a/packages/svelte/tests/parser-legacy/samples/action/output.json b/packages/svelte/tests/parser-legacy/samples/action/output.json index d72bf7db10..f241c81a93 100644 --- a/packages/svelte/tests/parser-legacy/samples/action/output.json +++ b/packages/svelte/tests/parser-legacy/samples/action/output.json @@ -15,8 +15,8 @@ "end": 20, "type": "Action", "name": "autofocus", - "modifiers": [], - "expression": null + "expression": null, + "modifiers": [] } ], "children": [] diff --git a/packages/svelte/tests/parser-legacy/samples/animation/output.json b/packages/svelte/tests/parser-legacy/samples/animation/output.json index 0d82cb2bb9..bf4b43b875 100644 --- a/packages/svelte/tests/parser-legacy/samples/animation/output.json +++ b/packages/svelte/tests/parser-legacy/samples/animation/output.json @@ -20,8 +20,8 @@ "end": 50, "type": "Animation", "name": "flip", - "modifiers": [], - "expression": null + "expression": null, + "modifiers": [] } ], "children": [ diff --git a/packages/svelte/tests/parser-legacy/samples/attribute-class-directive/output.json b/packages/svelte/tests/parser-legacy/samples/attribute-class-directive/output.json index 9efe9acf8d..3cd54b6647 100644 --- a/packages/svelte/tests/parser-legacy/samples/attribute-class-directive/output.json +++ b/packages/svelte/tests/parser-legacy/samples/attribute-class-directive/output.json @@ -15,7 +15,6 @@ "end": 22, "type": "Class", "name": "foo", - "modifiers": [], "expression": { "type": "Identifier", "start": 16, @@ -31,7 +30,8 @@ } }, "name": "isFoo" - } + }, + "modifiers": [] } ], "children": [] diff --git a/packages/svelte/tests/parser-legacy/samples/attribute-with-whitespace/output.json b/packages/svelte/tests/parser-legacy/samples/attribute-with-whitespace/output.json index 4d3a291808..2e45184be9 100644 --- a/packages/svelte/tests/parser-legacy/samples/attribute-with-whitespace/output.json +++ b/packages/svelte/tests/parser-legacy/samples/attribute-with-whitespace/output.json @@ -15,7 +15,6 @@ "end": 23, "type": "EventHandler", "name": "click", - "modifiers": [], "expression": { "type": "Identifier", "start": 19, @@ -31,7 +30,8 @@ } }, "name": "foo" - } + }, + "modifiers": [] } ], "children": [ diff --git a/packages/svelte/tests/parser-legacy/samples/binding-shorthand/output.json b/packages/svelte/tests/parser-legacy/samples/binding-shorthand/output.json index 6720146297..4289245705 100644 --- a/packages/svelte/tests/parser-legacy/samples/binding-shorthand/output.json +++ b/packages/svelte/tests/parser-legacy/samples/binding-shorthand/output.json @@ -22,13 +22,13 @@ "end": 46, "type": "Binding", "name": "foo", - "modifiers": [], "expression": { "start": 43, "end": 46, "type": "Identifier", "name": "foo" - } + }, + "modifiers": [] } ], "children": [] diff --git a/packages/svelte/tests/parser-legacy/samples/binding/output.json b/packages/svelte/tests/parser-legacy/samples/binding/output.json index 4ce069bd37..5256ede7bb 100644 --- a/packages/svelte/tests/parser-legacy/samples/binding/output.json +++ b/packages/svelte/tests/parser-legacy/samples/binding/output.json @@ -22,7 +22,6 @@ "end": 55, "type": "Binding", "name": "value", - "modifiers": [], "expression": { "type": "Identifier", "start": 50, @@ -38,7 +37,8 @@ } }, "name": "name" - } + }, + "modifiers": [] } ], "children": [] diff --git a/packages/svelte/tests/parser-legacy/samples/dynamic-import/output.json b/packages/svelte/tests/parser-legacy/samples/dynamic-import/output.json index a439b65dd0..ee19d58742 100644 --- a/packages/svelte/tests/parser-legacy/samples/dynamic-import/output.json +++ b/packages/svelte/tests/parser-legacy/samples/dynamic-import/output.json @@ -104,7 +104,8 @@ }, "value": "svelte", "raw": "'svelte'" - } + }, + "attributes": [] }, { "type": "ExpressionStatement", @@ -257,7 +258,8 @@ }, "value": "./foo.js", "raw": "'./foo.js'" - } + }, + "options": null }, "property": { "type": "Identifier", diff --git a/packages/svelte/tests/parser-legacy/samples/event-handler/output.json b/packages/svelte/tests/parser-legacy/samples/event-handler/output.json index 45b6256677..11ee562297 100644 --- a/packages/svelte/tests/parser-legacy/samples/event-handler/output.json +++ b/packages/svelte/tests/parser-legacy/samples/event-handler/output.json @@ -15,7 +15,6 @@ "end": 45, "type": "EventHandler", "name": "click", - "modifiers": [], "expression": { "type": "ArrowFunctionExpression", "start": 19, @@ -100,7 +99,8 @@ } } } - } + }, + "modifiers": [] } ], "children": [ diff --git a/packages/svelte/tests/parser-legacy/samples/generic-snippets/input.svelte b/packages/svelte/tests/parser-legacy/samples/generic-snippets/input.svelte new file mode 100644 index 0000000000..4ee619728d --- /dev/null +++ b/packages/svelte/tests/parser-legacy/samples/generic-snippets/input.svelte @@ -0,0 +1,10 @@ + + +{#snippet generic(val: T)} + {val} +{/snippet} + +{#snippet complex_generic">>(val: T)} + {val} +{/snippet} \ No newline at end of file diff --git a/packages/svelte/tests/parser-legacy/samples/generic-snippets/output.json b/packages/svelte/tests/parser-legacy/samples/generic-snippets/output.json new file mode 100644 index 0000000000..37fb499e7b --- /dev/null +++ b/packages/svelte/tests/parser-legacy/samples/generic-snippets/output.json @@ -0,0 +1,244 @@ +{ + "html": { + "type": "Fragment", + "start": 30, + "end": 192, + "children": [ + { + "type": "Text", + "start": 28, + "end": 30, + "raw": "\n\n", + "data": "\n\n" + }, + { + "type": "SnippetBlock", + "start": 30, + "end": 92, + "expression": { + "type": "Identifier", + "start": 40, + "end": 47, + "name": "generic" + }, + "parameters": [ + { + "type": "Identifier", + "start": 66, + "end": 72, + "loc": { + "start": { + "line": 4, + "column": 36 + }, + "end": { + "line": 4, + "column": 42 + } + }, + "name": "val", + "typeAnnotation": { + "type": "TSTypeAnnotation", + "start": 69, + "end": 72, + "loc": { + "start": { + "line": 4, + "column": 39 + }, + "end": { + "line": 4, + "column": 42 + } + }, + "typeAnnotation": { + "type": "TSTypeReference", + "start": 71, + "end": 72, + "loc": { + "start": { + "line": 4, + "column": 41 + }, + "end": { + "line": 4, + "column": 42 + } + }, + "typeName": { + "type": "Identifier", + "start": 71, + "end": 72, + "loc": { + "start": { + "line": 4, + "column": 41 + }, + "end": { + "line": 4, + "column": 42 + } + }, + "name": "T" + } + } + } + } + ], + "children": [ + { + "type": "MustacheTag", + "start": 76, + "end": 81, + "expression": { + "type": "Identifier", + "start": 77, + "end": 80, + "loc": { + "start": { + "line": 5, + "column": 2 + }, + "end": { + "line": 5, + "column": 5 + } + }, + "name": "val" + } + } + ], + "typeParams": "T extends string" + }, + { + "type": "Text", + "start": 92, + "end": 94, + "raw": "\n\n", + "data": "\n\n" + }, + { + "type": "SnippetBlock", + "start": 94, + "end": 192, + "expression": { + "type": "Identifier", + "start": 104, + "end": 119, + "name": "complex_generic" + }, + "parameters": [ + { + "type": "Identifier", + "start": 166, + "end": 172, + "loc": { + "start": { + "line": 8, + "column": 72 + }, + "end": { + "line": 8, + "column": 78 + } + }, + "name": "val", + "typeAnnotation": { + "type": "TSTypeAnnotation", + "start": 169, + "end": 172, + "loc": { + "start": { + "line": 8, + "column": 75 + }, + "end": { + "line": 8, + "column": 78 + } + }, + "typeAnnotation": { + "type": "TSTypeReference", + "start": 171, + "end": 172, + "loc": { + "start": { + "line": 8, + "column": 77 + }, + "end": { + "line": 8, + "column": 78 + } + }, + "typeName": { + "type": "Identifier", + "start": 171, + "end": 172, + "loc": { + "start": { + "line": 8, + "column": 77 + }, + "end": { + "line": 8, + "column": 78 + } + }, + "name": "T" + } + } + } + } + ], + "children": [ + { + "type": "MustacheTag", + "start": 176, + "end": 181, + "expression": { + "type": "Identifier", + "start": 177, + "end": 180, + "loc": { + "start": { + "line": 9, + "column": 2 + }, + "end": { + "line": 9, + "column": 5 + } + }, + "name": "val" + } + } + ], + "typeParams": "T extends { bracket: \"<\" } | \"<\" | Set<\"<>\">" + } + ] + }, + "instance": { + "type": "Script", + "start": 0, + "end": 28, + "context": "default", + "content": { + "type": "Program", + "start": 18, + "end": 19, + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 2, + "column": 0 + } + }, + "body": [], + "sourceType": "module" + } + } +} diff --git a/packages/svelte/tests/parser-legacy/samples/javascript-comments/output.json b/packages/svelte/tests/parser-legacy/samples/javascript-comments/output.json index 15db05904c..42229b741f 100644 --- a/packages/svelte/tests/parser-legacy/samples/javascript-comments/output.json +++ b/packages/svelte/tests/parser-legacy/samples/javascript-comments/output.json @@ -22,7 +22,6 @@ "end": 692, "type": "EventHandler", "name": "click", - "modifiers": [], "expression": { "type": "ArrowFunctionExpression", "start": 596, @@ -137,7 +136,8 @@ "end": 594 } ] - } + }, + "modifiers": [] } ], "children": [ diff --git a/packages/svelte/tests/parser-legacy/samples/refs/output.json b/packages/svelte/tests/parser-legacy/samples/refs/output.json index e2bda741fa..7829a2787f 100644 --- a/packages/svelte/tests/parser-legacy/samples/refs/output.json +++ b/packages/svelte/tests/parser-legacy/samples/refs/output.json @@ -22,7 +22,6 @@ "end": 53, "type": "Binding", "name": "this", - "modifiers": [], "expression": { "type": "Identifier", "start": 49, @@ -38,7 +37,8 @@ } }, "name": "foo" - } + }, + "modifiers": [] } ], "children": [] diff --git a/packages/svelte/tests/parser-legacy/samples/transition-intro-no-params/output.json b/packages/svelte/tests/parser-legacy/samples/transition-intro-no-params/output.json index f30788d758..18860d615b 100644 --- a/packages/svelte/tests/parser-legacy/samples/transition-intro-no-params/output.json +++ b/packages/svelte/tests/parser-legacy/samples/transition-intro-no-params/output.json @@ -15,8 +15,8 @@ "end": 12, "type": "Transition", "name": "fade", - "modifiers": [], "expression": null, + "modifiers": [], "intro": true, "outro": false } diff --git a/packages/svelte/tests/parser-legacy/samples/transition-intro/output.json b/packages/svelte/tests/parser-legacy/samples/transition-intro/output.json index ae52f72c5d..973cfb7d33 100644 --- a/packages/svelte/tests/parser-legacy/samples/transition-intro/output.json +++ b/packages/svelte/tests/parser-legacy/samples/transition-intro/output.json @@ -15,7 +15,6 @@ "end": 30, "type": "Transition", "name": "style", - "modifiers": [], "expression": { "type": "ObjectExpression", "start": 16, @@ -85,6 +84,7 @@ } ] }, + "modifiers": [], "intro": true, "outro": false } diff --git a/packages/svelte/tests/parser-modern/samples/attachments/input.svelte b/packages/svelte/tests/parser-modern/samples/attachments/input.svelte new file mode 100644 index 0000000000..9faae8d1bf --- /dev/null +++ b/packages/svelte/tests/parser-modern/samples/attachments/input.svelte @@ -0,0 +1 @@ +
{}} {@attach (node) => {}}>
diff --git a/packages/svelte/tests/parser-modern/samples/attachments/output.json b/packages/svelte/tests/parser-modern/samples/attachments/output.json new file mode 100644 index 0000000000..42e9880fcc --- /dev/null +++ b/packages/svelte/tests/parser-modern/samples/attachments/output.json @@ -0,0 +1,141 @@ +{ + "css": null, + "js": [], + "start": 0, + "end": 57, + "type": "Root", + "fragment": { + "type": "Fragment", + "nodes": [ + { + "type": "RegularElement", + "start": 0, + "end": 57, + "name": "div", + "attributes": [ + { + "type": "AttachTag", + "start": 5, + "end": 27, + "expression": { + "type": "ArrowFunctionExpression", + "start": 14, + "end": 26, + "loc": { + "start": { + "line": 1, + "column": 14 + }, + "end": { + "line": 1, + "column": 26 + } + }, + "id": null, + "expression": false, + "generator": false, + "async": false, + "params": [ + { + "type": "Identifier", + "start": 15, + "end": 19, + "loc": { + "start": { + "line": 1, + "column": 15 + }, + "end": { + "line": 1, + "column": 19 + } + }, + "name": "node" + } + ], + "body": { + "type": "BlockStatement", + "start": 24, + "end": 26, + "loc": { + "start": { + "line": 1, + "column": 24 + }, + "end": { + "line": 1, + "column": 26 + } + }, + "body": [] + } + } + }, + { + "type": "AttachTag", + "start": 28, + "end": 50, + "expression": { + "type": "ArrowFunctionExpression", + "start": 37, + "end": 49, + "loc": { + "start": { + "line": 1, + "column": 37 + }, + "end": { + "line": 1, + "column": 49 + } + }, + "id": null, + "expression": false, + "generator": false, + "async": false, + "params": [ + { + "type": "Identifier", + "start": 38, + "end": 42, + "loc": { + "start": { + "line": 1, + "column": 38 + }, + "end": { + "line": 1, + "column": 42 + } + }, + "name": "node" + } + ], + "body": { + "type": "BlockStatement", + "start": 47, + "end": 49, + "loc": { + "start": { + "line": 1, + "column": 47 + }, + "end": { + "line": 1, + "column": 49 + } + }, + "body": [] + } + } + } + ], + "fragment": { + "type": "Fragment", + "nodes": [] + } + } + ] + }, + "options": null +} diff --git a/packages/svelte/tests/parser-modern/samples/generic-snippets/input.svelte b/packages/svelte/tests/parser-modern/samples/generic-snippets/input.svelte new file mode 100644 index 0000000000..4ee619728d --- /dev/null +++ b/packages/svelte/tests/parser-modern/samples/generic-snippets/input.svelte @@ -0,0 +1,10 @@ + + +{#snippet generic(val: T)} + {val} +{/snippet} + +{#snippet complex_generic">>(val: T)} + {val} +{/snippet} \ No newline at end of file diff --git a/packages/svelte/tests/parser-modern/samples/generic-snippets/output.json b/packages/svelte/tests/parser-modern/samples/generic-snippets/output.json new file mode 100644 index 0000000000..b66ee7288f --- /dev/null +++ b/packages/svelte/tests/parser-modern/samples/generic-snippets/output.json @@ -0,0 +1,299 @@ +{ + "css": null, + "js": [], + "start": 30, + "end": 192, + "type": "Root", + "fragment": { + "type": "Fragment", + "nodes": [ + { + "type": "Text", + "start": 28, + "end": 30, + "raw": "\n\n", + "data": "\n\n" + }, + { + "type": "SnippetBlock", + "start": 30, + "end": 92, + "expression": { + "type": "Identifier", + "start": 40, + "end": 47, + "name": "generic" + }, + "typeParams": "T extends string", + "parameters": [ + { + "type": "Identifier", + "start": 66, + "end": 72, + "loc": { + "start": { + "line": 4, + "column": 36 + }, + "end": { + "line": 4, + "column": 42 + } + }, + "name": "val", + "typeAnnotation": { + "type": "TSTypeAnnotation", + "start": 69, + "end": 72, + "loc": { + "start": { + "line": 4, + "column": 39 + }, + "end": { + "line": 4, + "column": 42 + } + }, + "typeAnnotation": { + "type": "TSTypeReference", + "start": 71, + "end": 72, + "loc": { + "start": { + "line": 4, + "column": 41 + }, + "end": { + "line": 4, + "column": 42 + } + }, + "typeName": { + "type": "Identifier", + "start": 71, + "end": 72, + "loc": { + "start": { + "line": 4, + "column": 41 + }, + "end": { + "line": 4, + "column": 42 + } + }, + "name": "T" + } + } + } + } + ], + "body": { + "type": "Fragment", + "nodes": [ + { + "type": "Text", + "start": 74, + "end": 76, + "raw": "\n\t", + "data": "\n\t" + }, + { + "type": "ExpressionTag", + "start": 76, + "end": 81, + "expression": { + "type": "Identifier", + "start": 77, + "end": 80, + "loc": { + "start": { + "line": 5, + "column": 2 + }, + "end": { + "line": 5, + "column": 5 + } + }, + "name": "val" + } + }, + { + "type": "Text", + "start": 81, + "end": 82, + "raw": "\n", + "data": "\n" + } + ] + } + }, + { + "type": "Text", + "start": 92, + "end": 94, + "raw": "\n\n", + "data": "\n\n" + }, + { + "type": "SnippetBlock", + "start": 94, + "end": 192, + "expression": { + "type": "Identifier", + "start": 104, + "end": 119, + "name": "complex_generic" + }, + "typeParams": "T extends { bracket: \"<\" } | \"<\" | Set<\"<>\">", + "parameters": [ + { + "type": "Identifier", + "start": 166, + "end": 172, + "loc": { + "start": { + "line": 8, + "column": 72 + }, + "end": { + "line": 8, + "column": 78 + } + }, + "name": "val", + "typeAnnotation": { + "type": "TSTypeAnnotation", + "start": 169, + "end": 172, + "loc": { + "start": { + "line": 8, + "column": 75 + }, + "end": { + "line": 8, + "column": 78 + } + }, + "typeAnnotation": { + "type": "TSTypeReference", + "start": 171, + "end": 172, + "loc": { + "start": { + "line": 8, + "column": 77 + }, + "end": { + "line": 8, + "column": 78 + } + }, + "typeName": { + "type": "Identifier", + "start": 171, + "end": 172, + "loc": { + "start": { + "line": 8, + "column": 77 + }, + "end": { + "line": 8, + "column": 78 + } + }, + "name": "T" + } + } + } + } + ], + "body": { + "type": "Fragment", + "nodes": [ + { + "type": "Text", + "start": 174, + "end": 176, + "raw": "\n\t", + "data": "\n\t" + }, + { + "type": "ExpressionTag", + "start": 176, + "end": 181, + "expression": { + "type": "Identifier", + "start": 177, + "end": 180, + "loc": { + "start": { + "line": 9, + "column": 2 + }, + "end": { + "line": 9, + "column": 5 + } + }, + "name": "val" + } + }, + { + "type": "Text", + "start": 181, + "end": 182, + "raw": "\n", + "data": "\n" + } + ] + } + } + ] + }, + "options": null, + "instance": { + "type": "Script", + "start": 0, + "end": 28, + "context": "default", + "content": { + "type": "Program", + "start": 18, + "end": 19, + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 2, + "column": 0 + } + }, + "body": [], + "sourceType": "module" + }, + "attributes": [ + { + "type": "Attribute", + "start": 8, + "end": 17, + "name": "lang", + "value": [ + { + "start": 14, + "end": 16, + "type": "Text", + "raw": "ts", + "data": "ts" + } + ] + } + ] + } +} diff --git a/packages/svelte/tests/runtime-browser/custom-elements-samples/extend-with-ts/_config.js b/packages/svelte/tests/runtime-browser/custom-elements-samples/extend-with-ts/_config.js new file mode 100644 index 0000000000..6502a08290 --- /dev/null +++ b/packages/svelte/tests/runtime-browser/custom-elements-samples/extend-with-ts/_config.js @@ -0,0 +1,19 @@ +import { test } from '../../assert'; +const tick = () => Promise.resolve(); + +export default test({ + async test({ assert, target }) { + target.innerHTML = ''; + await tick(); + /** @type {any} */ + const el = target.querySelector('custom-element'); + + assert.htmlEqual( + el.shadowRoot.innerHTML, + ` +

name: world

+ ` + ); + assert.equal(el.test, `test`); + } +}); diff --git a/packages/svelte/tests/runtime-browser/custom-elements-samples/extend-with-ts/main.svelte b/packages/svelte/tests/runtime-browser/custom-elements-samples/extend-with-ts/main.svelte new file mode 100644 index 0000000000..ddd2d4b61a --- /dev/null +++ b/packages/svelte/tests/runtime-browser/custom-elements-samples/extend-with-ts/main.svelte @@ -0,0 +1,14 @@ +{ + return class extends customClass{ + public test: string = "test"; + } + }, +}}/> + + + +

name: {name}

\ No newline at end of file diff --git a/packages/svelte/tests/runtime-browser/samples/css-props-dynamic-component/A.svelte b/packages/svelte/tests/runtime-browser/samples/css-props-dynamic-component/A.svelte new file mode 100644 index 0000000000..694b26f231 --- /dev/null +++ b/packages/svelte/tests/runtime-browser/samples/css-props-dynamic-component/A.svelte @@ -0,0 +1,7 @@ +
a
+ + \ No newline at end of file diff --git a/packages/svelte/tests/runtime-browser/samples/css-props-dynamic-component/B.svelte b/packages/svelte/tests/runtime-browser/samples/css-props-dynamic-component/B.svelte new file mode 100644 index 0000000000..06f28c4f75 --- /dev/null +++ b/packages/svelte/tests/runtime-browser/samples/css-props-dynamic-component/B.svelte @@ -0,0 +1,7 @@ +
b
+ + \ No newline at end of file diff --git a/packages/svelte/tests/runtime-browser/samples/css-props-dynamic-component/_config.js b/packages/svelte/tests/runtime-browser/samples/css-props-dynamic-component/_config.js new file mode 100644 index 0000000000..7629633835 --- /dev/null +++ b/packages/svelte/tests/runtime-browser/samples/css-props-dynamic-component/_config.js @@ -0,0 +1,16 @@ +import { test } from '../../assert'; +import { flushSync } from 'svelte'; + +export default test({ + warnings: [], + async test({ assert, target }) { + const btn = target.querySelector('button'); + let div = /** @type {HTMLElement} */ (target.querySelector('div')); + assert.equal(getComputedStyle(div).color, 'rgb(255, 0, 0)'); + flushSync(() => { + btn?.click(); + }); + div = /** @type {HTMLElement} */ (target.querySelector('div')); + assert.equal(getComputedStyle(div).color, 'rgb(255, 0, 0)'); + } +}); diff --git a/packages/svelte/tests/runtime-browser/samples/css-props-dynamic-component/main.svelte b/packages/svelte/tests/runtime-browser/samples/css-props-dynamic-component/main.svelte new file mode 100644 index 0000000000..055ce57da5 --- /dev/null +++ b/packages/svelte/tests/runtime-browser/samples/css-props-dynamic-component/main.svelte @@ -0,0 +1,11 @@ + + + + + \ No newline at end of file diff --git a/packages/svelte/tests/runtime-browser/test.ts b/packages/svelte/tests/runtime-browser/test.ts index 582a10edf7..63e601b115 100644 --- a/packages/svelte/tests/runtime-browser/test.ts +++ b/packages/svelte/tests/runtime-browser/test.ts @@ -5,7 +5,7 @@ import * as path from 'node:path'; import { compile } from 'svelte/compiler'; import { afterAll, assert, beforeAll, describe } from 'vitest'; import { suite, suite_with_variants } from '../suite'; -import { write } from '../helpers'; +import { write, fragments } from '../helpers'; import type { Warning } from '#compiler'; const assert_file = path.resolve(__dirname, 'assert.js'); @@ -87,6 +87,7 @@ async function run_test( build.onLoad({ filter: /\.svelte$/ }, (args) => { const compiled = compile(fs.readFileSync(args.path, 'utf-8').replace(/\r/g, ''), { generate: 'client', + fragments, ...config.compileOptions, immutable: config.immutable, customElement: test_dir.includes('custom-elements-samples'), diff --git a/packages/svelte/tests/runtime-legacy/samples/attachment-in-mutated-state/_config.js b/packages/svelte/tests/runtime-legacy/samples/attachment-in-mutated-state/_config.js new file mode 100644 index 0000000000..5d37252358 --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/attachment-in-mutated-state/_config.js @@ -0,0 +1,16 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + test({ assert, target, logs }) { + assert.deepEqual(logs, ['up']); + + const button = target.querySelector('button'); + + flushSync(() => button?.click()); + assert.deepEqual(logs, ['up']); + + flushSync(() => button?.click()); + assert.deepEqual(logs, ['up', 'down']); + } +}); diff --git a/packages/svelte/tests/runtime-legacy/samples/attachment-in-mutated-state/main.svelte b/packages/svelte/tests/runtime-legacy/samples/attachment-in-mutated-state/main.svelte new file mode 100644 index 0000000000..fbec108c7a --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/attachment-in-mutated-state/main.svelte @@ -0,0 +1,15 @@ + + + + +{#if state.count < 2} +
+{/if} diff --git a/packages/svelte/tests/runtime-legacy/samples/binding-circular/_config.js b/packages/svelte/tests/runtime-legacy/samples/binding-circular/_config.js index 29ddac16ad..9d47f86aad 100644 --- a/packages/svelte/tests/runtime-legacy/samples/binding-circular/_config.js +++ b/packages/svelte/tests/runtime-legacy/samples/binding-circular/_config.js @@ -1,9 +1,9 @@ import { test } from '../../test'; export default test({ - html: ` + ssrHtml: ` ` }); diff --git a/packages/svelte/tests/runtime-legacy/samples/binding-indirect/_config.js b/packages/svelte/tests/runtime-legacy/samples/binding-indirect/_config.js index d0b46cffa6..6e1feb70fa 100644 --- a/packages/svelte/tests/runtime-legacy/samples/binding-indirect/_config.js +++ b/packages/svelte/tests/runtime-legacy/samples/binding-indirect/_config.js @@ -17,9 +17,9 @@ export default test({ return { tasks, selected: tasks[0] }; }, - html: ` + ssrHtml: ` + + + + + + + + +

Pending tasks

+

put your left leg in

+

your left leg out

+

in, out, in, out

+

shake it all about

+ ` + ); const input = target.querySelector('input'); const select = target.querySelector('select'); const options = target.querySelectorAll('option'); @@ -57,7 +78,7 @@ export default test({ target.innerHTML, ` - + diff --git a/packages/svelte/tests/runtime-legacy/samples/binding-input-text-contextual-reactive-prop/_config.js b/packages/svelte/tests/runtime-legacy/samples/binding-input-text-contextual-reactive-prop/_config.js new file mode 100644 index 0000000000..d20e4a1e88 --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/binding-input-text-contextual-reactive-prop/_config.js @@ -0,0 +1,156 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + get props() { + return { + items: [ + { done: false, text: 'one' }, + { done: true, text: 'two' }, + { done: false, text: 'three' } + ] + }; + }, + + html: ` +
+ +

one

+
+
+ +

two

+
+
+ +

three

+
+ +

remaining:one / done:two / remaining:three

+ `, + + ssrHtml: ` +
+ +

one

+
+
+ +

two

+
+
+ +

three

+
+ +

remaining:one / done:two / remaining:three

+ `, + + test({ assert, component, target, window }) { + /** + * @param {number} i + * @param {string} text + */ + function set_text(i, text) { + const input = /** @type {HTMLInputElement} */ ( + target.querySelectorAll('input[type="text"]')[i] + ); + input.value = text; + input.dispatchEvent(new window.Event('input')); + } + + /** + * @param {number} i + * @param {boolean} done + */ + function set_done(i, done) { + const input = /** @type {HTMLInputElement} */ ( + target.querySelectorAll('input[type="checkbox"]')[i] + ); + input.checked = done; + input.dispatchEvent(new window.Event('change')); + } + + component.filter = 'remaining'; + + assert.htmlEqual( + target.innerHTML, + ` +
+ +

one

+
+
+ +

three

+
+ +

remaining:one / done:two / remaining:three

+ ` + ); + + set_text(1, 'four'); + flushSync(); + + assert.htmlEqual( + target.innerHTML, + ` +
+ +

one

+
+
+ +

four

+
+ +

remaining:one / done:two / remaining:four

+ ` + ); + + assert.deepEqual(component.items, [ + { done: false, text: 'one' }, + { done: true, text: 'two' }, + { done: false, text: 'four' } + ]); + + set_done(0, true); + flushSync(); + + assert.htmlEqual( + target.innerHTML, + ` +
+ +

four

+
+ +

done:one / done:two / remaining:four

+ ` + ); + + assert.deepEqual(component.items, [ + { done: true, text: 'one' }, + { done: true, text: 'two' }, + { done: false, text: 'four' } + ]); + + component.filter = 'done'; + + assert.htmlEqual( + target.innerHTML, + ` +
+ +

one

+
+
+ +

two

+
+ +

done:one / done:two / remaining:four

+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-legacy/samples/binding-input-text-contextual-reactive-prop/main.svelte b/packages/svelte/tests/runtime-legacy/samples/binding-input-text-contextual-reactive-prop/main.svelte new file mode 100644 index 0000000000..7e10199530 --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/binding-input-text-contextual-reactive-prop/main.svelte @@ -0,0 +1,25 @@ + + +{#each filtered as item} +
+ + +

{item.text}

+
+{/each} + +

{summary}

\ No newline at end of file diff --git a/packages/svelte/tests/runtime-legacy/samples/binding-input-text-contextual-reactive/_config.js b/packages/svelte/tests/runtime-legacy/samples/binding-input-text-contextual-reactive/_config.js index d20e4a1e88..c8019e11e9 100644 --- a/packages/svelte/tests/runtime-legacy/samples/binding-input-text-contextual-reactive/_config.js +++ b/packages/svelte/tests/runtime-legacy/samples/binding-input-text-contextual-reactive/_config.js @@ -2,16 +2,6 @@ import { flushSync } from 'svelte'; import { test } from '../../test'; export default test({ - get props() { - return { - items: [ - { done: false, text: 'one' }, - { done: true, text: 'two' }, - { done: false, text: 'three' } - ] - }; - }, - html: `
@@ -108,12 +98,6 @@ export default test({ ` ); - assert.deepEqual(component.items, [ - { done: false, text: 'one' }, - { done: true, text: 'two' }, - { done: false, text: 'four' } - ]); - set_done(0, true); flushSync(); @@ -129,12 +113,6 @@ export default test({ ` ); - assert.deepEqual(component.items, [ - { done: true, text: 'one' }, - { done: true, text: 'two' }, - { done: false, text: 'four' } - ]); - component.filter = 'done'; assert.htmlEqual( diff --git a/packages/svelte/tests/runtime-legacy/samples/binding-input-text-contextual-reactive/main.svelte b/packages/svelte/tests/runtime-legacy/samples/binding-input-text-contextual-reactive/main.svelte index 7e10199530..9ca7832a72 100644 --- a/packages/svelte/tests/runtime-legacy/samples/binding-input-text-contextual-reactive/main.svelte +++ b/packages/svelte/tests/runtime-legacy/samples/binding-input-text-contextual-reactive/main.svelte @@ -1,5 +1,10 @@ + +
diff --git a/packages/svelte/tests/runtime-runes/samples/attachment-component-falsy/_config.js b/packages/svelte/tests/runtime-runes/samples/attachment-component-falsy/_config.js new file mode 100644 index 0000000000..27f013d6d1 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/attachment-component-falsy/_config.js @@ -0,0 +1,5 @@ +import { test } from '../../test'; + +export default test({ + test() {} +}); diff --git a/packages/svelte/tests/runtime-runes/samples/attachment-component-falsy/main.svelte b/packages/svelte/tests/runtime-runes/samples/attachment-component-falsy/main.svelte new file mode 100644 index 0000000000..993bcdd6a5 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/attachment-component-falsy/main.svelte @@ -0,0 +1,13 @@ + + + + + diff --git a/packages/svelte/tests/runtime-runes/samples/attachment-component-spread/Child.svelte b/packages/svelte/tests/runtime-runes/samples/attachment-component-spread/Child.svelte new file mode 100644 index 0000000000..6760da61fa --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/attachment-component-spread/Child.svelte @@ -0,0 +1,5 @@ + + +
diff --git a/packages/svelte/tests/runtime-runes/samples/attachment-component-spread/_config.js b/packages/svelte/tests/runtime-runes/samples/attachment-component-spread/_config.js new file mode 100644 index 0000000000..23907c62d2 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/attachment-component-spread/_config.js @@ -0,0 +1,15 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: `
`, + + test({ target, assert, logs }) { + const button = target.querySelector('button'); + + assert.deepEqual(logs, ['one DIV']); + + flushSync(() => button?.click()); + assert.deepEqual(logs, ['one DIV', 'cleanup one', 'two DIV']); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/attachment-component-spread/main.svelte b/packages/svelte/tests/runtime-runes/samples/attachment-component-spread/main.svelte new file mode 100644 index 0000000000..d1d7e65126 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/attachment-component-spread/main.svelte @@ -0,0 +1,30 @@ + + + + + diff --git a/packages/svelte/tests/runtime-runes/samples/attachment-component/Child.svelte b/packages/svelte/tests/runtime-runes/samples/attachment-component/Child.svelte new file mode 100644 index 0000000000..6760da61fa --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/attachment-component/Child.svelte @@ -0,0 +1,5 @@ + + +
diff --git a/packages/svelte/tests/runtime-runes/samples/attachment-component/_config.js b/packages/svelte/tests/runtime-runes/samples/attachment-component/_config.js new file mode 100644 index 0000000000..b6ef016be5 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/attachment-component/_config.js @@ -0,0 +1,14 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + ssrHtml: `
`, + html: `
one
`, + + test({ target, assert }) { + const button = target.querySelector('button'); + + flushSync(() => button?.click()); + assert.htmlEqual(target.innerHTML, '
two
'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/attachment-component/main.svelte b/packages/svelte/tests/runtime-runes/samples/attachment-component/main.svelte new file mode 100644 index 0000000000..29e26689db --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/attachment-component/main.svelte @@ -0,0 +1,15 @@ + + + + + diff --git a/packages/svelte/tests/runtime-runes/samples/attachment-from-action/_config.js b/packages/svelte/tests/runtime-runes/samples/attachment-from-action/_config.js new file mode 100644 index 0000000000..2e53f0d29d --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/attachment-from-action/_config.js @@ -0,0 +1,116 @@ +import { ok, test } from '../../test'; +import { flushSync } from 'svelte'; + +export default test({ + async test({ assert, target, logs }) { + const [btn, btn2, btn3] = target.querySelectorAll('button'); + + // both logs on creation it will not log on change + assert.deepEqual(logs, ['create', 0, 'action', 'create', 0, 'attachment']); + + // clicking the first button logs the right value + flushSync(() => { + btn?.click(); + }); + assert.deepEqual(logs, ['create', 0, 'action', 'create', 0, 'attachment', 0]); + + // clicking the second button logs the right value + flushSync(() => { + btn2?.click(); + }); + assert.deepEqual(logs, ['create', 0, 'action', 'create', 0, 'attachment', 0, 0]); + + // updating the arguments logs the update function for both + flushSync(() => { + btn3?.click(); + }); + assert.deepEqual(logs, [ + 'create', + 0, + 'action', + 'create', + 0, + 'attachment', + 0, + 0, + 'update', + 1, + 'action', + 'update', + 1, + 'attachment' + ]); + + // clicking the first button again shows the right value + flushSync(() => { + btn?.click(); + }); + assert.deepEqual(logs, [ + 'create', + 0, + 'action', + 'create', + 0, + 'attachment', + 0, + 0, + 'update', + 1, + 'action', + 'update', + 1, + 'attachment', + 1 + ]); + + // clicking the second button again shows the right value + flushSync(() => { + btn2?.click(); + }); + assert.deepEqual(logs, [ + 'create', + 0, + 'action', + 'create', + 0, + 'attachment', + 0, + 0, + 'update', + 1, + 'action', + 'update', + 1, + 'attachment', + 1, + 1 + ]); + + // unmounting logs the destroy function for both + flushSync(() => { + btn3?.click(); + }); + assert.deepEqual(logs, [ + 'create', + 0, + 'action', + 'create', + 0, + 'attachment', + 0, + 0, + 'update', + 1, + 'action', + 'update', + 1, + 'attachment', + 1, + 1, + 'destroy', + 'action', + 'destroy', + 'attachment' + ]); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/attachment-from-action/main.svelte b/packages/svelte/tests/runtime-runes/samples/attachment-from-action/main.svelte new file mode 100644 index 0000000000..35079aa15e --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/attachment-from-action/main.svelte @@ -0,0 +1,37 @@ + + +{#if count < 2} + + +{/if} + + \ No newline at end of file diff --git a/packages/svelte/tests/runtime-runes/samples/attachment-reactive/_config.js b/packages/svelte/tests/runtime-runes/samples/attachment-reactive/_config.js new file mode 100644 index 0000000000..7d0502590b --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/attachment-reactive/_config.js @@ -0,0 +1,14 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + ssrHtml: `
`, + html: `
1
`, + + test: ({ assert, target }) => { + const btn = target.querySelector('button'); + + flushSync(() => btn?.click()); + assert.htmlEqual(target.innerHTML, `
2
`); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/attachment-reactive/main.svelte b/packages/svelte/tests/runtime-runes/samples/attachment-reactive/main.svelte new file mode 100644 index 0000000000..9fa3cfdb67 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/attachment-reactive/main.svelte @@ -0,0 +1,6 @@ + + +
node.textContent = value}>
+ diff --git a/packages/svelte/tests/runtime-runes/samples/attachment-spread-stable/Component.svelte b/packages/svelte/tests/runtime-runes/samples/attachment-spread-stable/Component.svelte new file mode 100644 index 0000000000..d5f4e94358 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/attachment-spread-stable/Component.svelte @@ -0,0 +1,5 @@ + + +

hello

diff --git a/packages/svelte/tests/runtime-runes/samples/attachment-spread-stable/_config.js b/packages/svelte/tests/runtime-runes/samples/attachment-spread-stable/_config.js new file mode 100644 index 0000000000..5d37252358 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/attachment-spread-stable/_config.js @@ -0,0 +1,16 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + test({ assert, target, logs }) { + assert.deepEqual(logs, ['up']); + + const button = target.querySelector('button'); + + flushSync(() => button?.click()); + assert.deepEqual(logs, ['up']); + + flushSync(() => button?.click()); + assert.deepEqual(logs, ['up', 'down']); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/attachment-spread-stable/main.svelte b/packages/svelte/tests/runtime-runes/samples/attachment-spread-stable/main.svelte new file mode 100644 index 0000000000..aef9da6dd4 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/attachment-spread-stable/main.svelte @@ -0,0 +1,17 @@ + + + + +{#if count < 2} + { + console.log('up'); + return () => console.log('down'); + }} + /> +{/if} diff --git a/packages/svelte/tests/runtime-runes/samples/attachment-spread/_config.js b/packages/svelte/tests/runtime-runes/samples/attachment-spread/_config.js new file mode 100644 index 0000000000..96fc207450 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/attachment-spread/_config.js @@ -0,0 +1,8 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + test({ assert, logs, target }) { + assert.deepEqual(logs, ['hello']); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/attachment-spread/main.svelte b/packages/svelte/tests/runtime-runes/samples/attachment-spread/main.svelte new file mode 100644 index 0000000000..dbd8c47ada --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/attachment-spread/main.svelte @@ -0,0 +1,9 @@ + + +
diff --git a/packages/svelte/tests/runtime-runes/samples/attachment-svelte-element/_config.js b/packages/svelte/tests/runtime-runes/samples/attachment-svelte-element/_config.js new file mode 100644 index 0000000000..1be4737069 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/attachment-svelte-element/_config.js @@ -0,0 +1,6 @@ +import { test } from '../../test'; + +export default test({ + ssrHtml: `
`, + html: `
DIV
` +}); diff --git a/packages/svelte/tests/runtime-runes/samples/attachment-svelte-element/main.svelte b/packages/svelte/tests/runtime-runes/samples/attachment-svelte-element/main.svelte new file mode 100644 index 0000000000..bd4b52342f --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/attachment-svelte-element/main.svelte @@ -0,0 +1 @@ + node.textContent = node.nodeName}> diff --git a/packages/svelte/tests/runtime-runes/samples/bigint-invalid/_config.js b/packages/svelte/tests/runtime-runes/samples/bigint-invalid/_config.js new file mode 100644 index 0000000000..810ac338a5 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/bigint-invalid/_config.js @@ -0,0 +1,7 @@ +import { test } from '../../test'; + +export default test({ + // check that this is a runtime error, not a compile time error + // caused by over-eager partial-evaluation + error: 'Cannot convert invalid to a BigInt' +}); diff --git a/packages/svelte/tests/runtime-runes/samples/bigint-invalid/main.svelte b/packages/svelte/tests/runtime-runes/samples/bigint-invalid/main.svelte new file mode 100644 index 0000000000..126528bad2 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/bigint-invalid/main.svelte @@ -0,0 +1,5 @@ + + +

{invalid}

diff --git a/packages/svelte/tests/runtime-runes/samples/bindings-form-reset/main.svelte b/packages/svelte/tests/runtime-runes/samples/bindings-form-reset/main.svelte index ff13af85c8..0886efd59c 100644 --- a/packages/svelte/tests/runtime-runes/samples/bindings-form-reset/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/bindings-form-reset/main.svelte @@ -3,8 +3,14 @@ let checkbox = $state(true); let radio_group = $state('a'); let checkbox_group = $state(['a']); - let select = $state('b'); + // this will be ssrd + let select = $state('a'); let textarea = $state('textarea'); + + $effect(()=>{ + // changing the value of `select` on mount + select = 'b'; + })

{JSON.stringify({ text, checkbox, radio_group, checkbox_group, select, textarea })}

diff --git a/packages/svelte/tests/runtime-runes/samples/class-private-fields-assignment-shorthand/_config.js b/packages/svelte/tests/runtime-runes/samples/class-private-fields-assignment-shorthand/_config.js new file mode 100644 index 0000000000..0fdeabfe0b --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/class-private-fields-assignment-shorthand/_config.js @@ -0,0 +1,20 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + const btn = target.querySelector('button'); + + btn?.click(); + flushSync(); + assert.htmlEqual( + target.innerHTML, + ` + +

a:1

+

b:2

+

c:3

+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/class-private-fields-assignment-shorthand/main.svelte b/packages/svelte/tests/runtime-runes/samples/class-private-fields-assignment-shorthand/main.svelte new file mode 100644 index 0000000000..746f22b1e6 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/class-private-fields-assignment-shorthand/main.svelte @@ -0,0 +1,28 @@ + + + + +{#key 1}

a:{counter.a}

{/key} +{#key 2}

b:{counter.b}

{/key} +{#key 3}

c:{counter.c}

{/key} diff --git a/packages/svelte/tests/runtime-runes/samples/class-state-constructor-closure-private/_config.js b/packages/svelte/tests/runtime-runes/samples/class-state-constructor-closure-private-1/_config.js similarity index 100% rename from packages/svelte/tests/runtime-runes/samples/class-state-constructor-closure-private/_config.js rename to packages/svelte/tests/runtime-runes/samples/class-state-constructor-closure-private-1/_config.js diff --git a/packages/svelte/tests/runtime-runes/samples/class-state-constructor-closure-private/main.svelte b/packages/svelte/tests/runtime-runes/samples/class-state-constructor-closure-private-1/main.svelte similarity index 100% rename from packages/svelte/tests/runtime-runes/samples/class-state-constructor-closure-private/main.svelte rename to packages/svelte/tests/runtime-runes/samples/class-state-constructor-closure-private-1/main.svelte diff --git a/packages/svelte/tests/runtime-runes/samples/class-state-constructor-closure-private-2/_config.js b/packages/svelte/tests/runtime-runes/samples/class-state-constructor-closure-private-2/_config.js new file mode 100644 index 0000000000..dd847ce2f2 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/class-state-constructor-closure-private-2/_config.js @@ -0,0 +1,13 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: ``, + ssrHtml: ``, + + async test({ assert, target }) { + flushSync(); + + assert.htmlEqual(target.innerHTML, ``); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/class-state-constructor-closure-private-2/main.svelte b/packages/svelte/tests/runtime-runes/samples/class-state-constructor-closure-private-2/main.svelte new file mode 100644 index 0000000000..3d8ea41418 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/class-state-constructor-closure-private-2/main.svelte @@ -0,0 +1,13 @@ + + + diff --git a/packages/svelte/tests/runtime-runes/samples/class-state-constructor-closure-private-3/_config.js b/packages/svelte/tests/runtime-runes/samples/class-state-constructor-closure-private-3/_config.js new file mode 100644 index 0000000000..dd847ce2f2 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/class-state-constructor-closure-private-3/_config.js @@ -0,0 +1,13 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: ``, + ssrHtml: ``, + + async test({ assert, target }) { + flushSync(); + + assert.htmlEqual(target.innerHTML, ``); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/class-state-constructor-closure-private-3/main.svelte b/packages/svelte/tests/runtime-runes/samples/class-state-constructor-closure-private-3/main.svelte new file mode 100644 index 0000000000..47b8c901eb --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/class-state-constructor-closure-private-3/main.svelte @@ -0,0 +1,12 @@ + + + diff --git a/packages/svelte/tests/runtime-runes/samples/class-state-constructor-conflicting-get-name/_config.js b/packages/svelte/tests/runtime-runes/samples/class-state-constructor-conflicting-get-name/_config.js new file mode 100644 index 0000000000..f47bee71df --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/class-state-constructor-conflicting-get-name/_config.js @@ -0,0 +1,3 @@ +import { test } from '../../test'; + +export default test({}); diff --git a/packages/svelte/tests/runtime-runes/samples/class-state-constructor-conflicting-get-name/main.svelte b/packages/svelte/tests/runtime-runes/samples/class-state-constructor-conflicting-get-name/main.svelte new file mode 100644 index 0000000000..e2c4f302b3 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/class-state-constructor-conflicting-get-name/main.svelte @@ -0,0 +1,9 @@ + \ No newline at end of file diff --git a/packages/svelte/tests/runtime-runes/samples/class-state-constructor-derived-unowned/_config.js b/packages/svelte/tests/runtime-runes/samples/class-state-constructor-derived-unowned/_config.js new file mode 100644 index 0000000000..4cf1aea213 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/class-state-constructor-derived-unowned/_config.js @@ -0,0 +1,45 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + // The component context class instance gets shared between tests, strangely, causing hydration to fail? + mode: ['client', 'server'], + + async test({ assert, target, logs }) { + const btn = target.querySelector('button'); + + flushSync(() => { + btn?.click(); + }); + + assert.deepEqual(logs, [0, 'class trigger false', 'local trigger false', 1]); + + flushSync(() => { + btn?.click(); + }); + + assert.deepEqual(logs, [0, 'class trigger false', 'local trigger false', 1, 2]); + + flushSync(() => { + btn?.click(); + }); + + assert.deepEqual(logs, [0, 'class trigger false', 'local trigger false', 1, 2, 3]); + + flushSync(() => { + btn?.click(); + }); + + assert.deepEqual(logs, [ + 0, + 'class trigger false', + 'local trigger false', + 1, + 2, + 3, + 4, + 'class trigger true', + 'local trigger true' + ]); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/class-state-constructor-derived-unowned/main.svelte b/packages/svelte/tests/runtime-runes/samples/class-state-constructor-derived-unowned/main.svelte new file mode 100644 index 0000000000..03687d01bb --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/class-state-constructor-derived-unowned/main.svelte @@ -0,0 +1,37 @@ + + + + + diff --git a/packages/svelte/tests/runtime-runes/samples/class-state-constructor-predeclared-field/_config.js b/packages/svelte/tests/runtime-runes/samples/class-state-constructor-predeclared-field/_config.js new file mode 100644 index 0000000000..02cf36d900 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/class-state-constructor-predeclared-field/_config.js @@ -0,0 +1,20 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: ``, + + test({ assert, target }) { + const btn = target.querySelector('button'); + + flushSync(() => { + btn?.click(); + }); + assert.htmlEqual(target.innerHTML, ``); + + flushSync(() => { + btn?.click(); + }); + assert.htmlEqual(target.innerHTML, ``); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/class-state-constructor-predeclared-field/main.svelte b/packages/svelte/tests/runtime-runes/samples/class-state-constructor-predeclared-field/main.svelte new file mode 100644 index 0000000000..5dbbb10afd --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/class-state-constructor-predeclared-field/main.svelte @@ -0,0 +1,12 @@ + + + diff --git a/packages/svelte/tests/runtime-runes/samples/class-state-constructor-subclass/_config.js b/packages/svelte/tests/runtime-runes/samples/class-state-constructor-subclass/_config.js new file mode 100644 index 0000000000..32cca6c693 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/class-state-constructor-subclass/_config.js @@ -0,0 +1,20 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: ``, + + test({ assert, target }) { + const btn = target.querySelector('button'); + + flushSync(() => { + btn?.click(); + }); + assert.htmlEqual(target.innerHTML, ``); + + flushSync(() => { + btn?.click(); + }); + assert.htmlEqual(target.innerHTML, ``); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/class-state-constructor-subclass/main.svelte b/packages/svelte/tests/runtime-runes/samples/class-state-constructor-subclass/main.svelte new file mode 100644 index 0000000000..d8feb554cd --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/class-state-constructor-subclass/main.svelte @@ -0,0 +1,22 @@ + + + diff --git a/packages/svelte/tests/runtime-runes/samples/class-state-constructor/_config.js b/packages/svelte/tests/runtime-runes/samples/class-state-constructor/_config.js new file mode 100644 index 0000000000..f35dc57228 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/class-state-constructor/_config.js @@ -0,0 +1,20 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: ``, + + test({ assert, target }) { + const btn = target.querySelector('button'); + + flushSync(() => { + btn?.click(); + }); + assert.htmlEqual(target.innerHTML, ``); + + flushSync(() => { + btn?.click(); + }); + assert.htmlEqual(target.innerHTML, ``); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/class-state-constructor/main.svelte b/packages/svelte/tests/runtime-runes/samples/class-state-constructor/main.svelte new file mode 100644 index 0000000000..aa8ba1658b --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/class-state-constructor/main.svelte @@ -0,0 +1,18 @@ + + + diff --git a/packages/svelte/tests/runtime-runes/samples/class-state-derived-private/_config.js b/packages/svelte/tests/runtime-runes/samples/class-state-derived-private/_config.js index 141d994a2f..40ef84a2e6 100644 --- a/packages/svelte/tests/runtime-runes/samples/class-state-derived-private/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/class-state-derived-private/_config.js @@ -5,6 +5,7 @@ export default test({ html: `

doubled: 0

+

tripled: 0

`, test({ assert, target }) { @@ -17,6 +18,7 @@ export default test({ `

doubled: 2

+

tripled: 3

` ); @@ -27,6 +29,7 @@ export default test({ `

doubled: 4

+

tripled: 6

` ); } diff --git a/packages/svelte/tests/runtime-runes/samples/class-state-derived-private/main.svelte b/packages/svelte/tests/runtime-runes/samples/class-state-derived-private/main.svelte index 2c4c8f1839..d971566396 100644 --- a/packages/svelte/tests/runtime-runes/samples/class-state-derived-private/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/class-state-derived-private/main.svelte @@ -2,14 +2,24 @@ class Counter { count = $state(0); #doubled = $derived(this.count * 2); + #tripled = $derived.by(() => this.count * this.by); - get embiggened() { + constructor(by) { + this.by = by; + } + + get embiggened1() { return this.#doubled; } + + get embiggened2() { + return this.#tripled; + } } - const counter = new Counter(); + const counter = new Counter(3); -

doubled: {counter.embiggened}

+

doubled: {counter.embiggened1}

+

tripled: {counter.embiggened2}

diff --git a/packages/svelte/tests/runtime-runes/samples/custom-element-attributes/main.svelte b/packages/svelte/tests/runtime-runes/samples/custom-element-attributes/main.svelte index 4c98245e5b..82774f160d 100644 --- a/packages/svelte/tests/runtime-runes/samples/custom-element-attributes/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/custom-element-attributes/main.svelte @@ -1,18 +1,20 @@ diff --git a/packages/svelte/tests/runtime-runes/samples/derived-destructured-iterator/_config.js b/packages/svelte/tests/runtime-runes/samples/derived-destructured-iterator/_config.js new file mode 100644 index 0000000000..7f8d1e000d --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/derived-destructured-iterator/_config.js @@ -0,0 +1,16 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: `

a: 1

b: 2

c: 3

`, + + test({ assert, target }) { + const button = target.querySelector('button'); + + flushSync(() => button?.click()); + assert.htmlEqual( + target.innerHTML, + `

a: 2

b: 3

c: 4

` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/derived-destructured-iterator/main.svelte b/packages/svelte/tests/runtime-runes/samples/derived-destructured-iterator/main.svelte new file mode 100644 index 0000000000..8c8629a72c --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/derived-destructured-iterator/main.svelte @@ -0,0 +1,16 @@ + + + + +

a: {a}

+

b: {b}

+

c: {c}

diff --git a/packages/svelte/tests/runtime-runes/samples/destructure-state-from-props/Child.svelte b/packages/svelte/tests/runtime-runes/samples/destructure-state-from-props/Child.svelte new file mode 100644 index 0000000000..0996a02b60 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/destructure-state-from-props/Child.svelte @@ -0,0 +1,6 @@ + + +{foo} \ No newline at end of file diff --git a/packages/svelte/tests/runtime-runes/samples/destructure-state-from-props/_config.js b/packages/svelte/tests/runtime-runes/samples/destructure-state-from-props/_config.js new file mode 100644 index 0000000000..345654535b --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/destructure-state-from-props/_config.js @@ -0,0 +1,5 @@ +import { test } from '../../test'; + +export default test({ + html: `bar` +}); diff --git a/packages/svelte/tests/runtime-runes/samples/destructure-state-from-props/main.svelte b/packages/svelte/tests/runtime-runes/samples/destructure-state-from-props/main.svelte new file mode 100644 index 0000000000..518f733144 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/destructure-state-from-props/main.svelte @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/packages/svelte/tests/runtime-runes/samples/effect-teardown-stale-value/_config.js b/packages/svelte/tests/runtime-runes/samples/effect-teardown-stale-value/_config.js new file mode 100644 index 0000000000..1016ffb43e --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/effect-teardown-stale-value/_config.js @@ -0,0 +1,22 @@ +import { test } from '../../test'; +import { flushSync } from 'svelte'; + +export default test({ + html: ``, + + async test({ assert, target, logs }) { + assert.deepEqual(logs, ['up', { foo: false, bar: false }]); + + const button = target.querySelector('button'); + + flushSync(() => button?.click()); + assert.deepEqual(logs, [ + 'up', + { foo: false, bar: false }, + 'down', + { foo: false, bar: false }, + 'up', + { foo: true, bar: true } + ]); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/effect-teardown-stale-value/main.svelte b/packages/svelte/tests/runtime-runes/samples/effect-teardown-stale-value/main.svelte new file mode 100644 index 0000000000..fff81591d5 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/effect-teardown-stale-value/main.svelte @@ -0,0 +1,14 @@ + + + diff --git a/packages/svelte/tests/runtime-runes/samples/error-boundary-12/_config.js b/packages/svelte/tests/runtime-runes/samples/error-boundary-12/_config.js index b3edfefa20..4e771dfafa 100644 --- a/packages/svelte/tests/runtime-runes/samples/error-boundary-12/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/error-boundary-12/_config.js @@ -5,9 +5,8 @@ export default test({ test({ assert, target }) { const btn = target.querySelector('button'); - btn?.click(); - flushSync(); - - assert.htmlEqual(target.innerHTML, `

Error occured

`); + assert.throws(() => { + flushSync(() => btn?.click()); + }, /kaboom/); } }); diff --git a/packages/svelte/tests/runtime-runes/samples/error-boundary-12/main.svelte b/packages/svelte/tests/runtime-runes/samples/error-boundary-12/main.svelte index bae46c2590..d9dee1e2b0 100644 --- a/packages/svelte/tests/runtime-runes/samples/error-boundary-12/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/error-boundary-12/main.svelte @@ -1,20 +1,18 @@ - {#each things as thing} -

{thing}

- {/each} + {d} {#snippet failed()}

Error occured

diff --git a/packages/svelte/tests/runtime-runes/samples/error-boundary-13/Child.svelte b/packages/svelte/tests/runtime-runes/samples/error-boundary-13/Child.svelte index 6e607871d3..de482da985 100644 --- a/packages/svelte/tests/runtime-runes/samples/error-boundary-13/Child.svelte +++ b/packages/svelte/tests/runtime-runes/samples/error-boundary-13/Child.svelte @@ -1,7 +1,14 @@ diff --git a/packages/svelte/tests/runtime-runes/samples/error-boundary-13/_config.js b/packages/svelte/tests/runtime-runes/samples/error-boundary-13/_config.js index b3edfefa20..3825dc7811 100644 --- a/packages/svelte/tests/runtime-runes/samples/error-boundary-13/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/error-boundary-13/_config.js @@ -8,6 +8,6 @@ export default test({ btn?.click(); flushSync(); - assert.htmlEqual(target.innerHTML, `

Error occured

`); + assert.htmlEqual(target.innerHTML, `

Error occurred

`); } }); diff --git a/packages/svelte/tests/runtime-runes/samples/error-boundary-13/main.svelte b/packages/svelte/tests/runtime-runes/samples/error-boundary-13/main.svelte index 65b71106fa..56e4846675 100644 --- a/packages/svelte/tests/runtime-runes/samples/error-boundary-13/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/error-boundary-13/main.svelte @@ -2,21 +2,14 @@ import Child from './Child.svelte'; let count = $state(0); - - const things = $derived.by(() => { - if (count === 1) { - throw new Error('123') - } - return [1, 2 ,3] - }) - + {#snippet failed()} -

Error occured

+

Error occurred

{/snippet}
diff --git a/packages/svelte/tests/runtime-runes/samples/error-boundary-18/_config.js b/packages/svelte/tests/runtime-runes/samples/error-boundary-18/_config.js index fedaaa9ad1..e092d0e7c7 100644 --- a/packages/svelte/tests/runtime-runes/samples/error-boundary-18/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/error-boundary-18/_config.js @@ -5,11 +5,11 @@ export default test({ test({ assert, target }) { let btn = target.querySelector('button'); - btn?.click(); - btn?.click(); - assert.throws(() => { - flushSync(); + flushSync(() => { + btn?.click(); + btn?.click(); + }); }, /test\n\n\tin {expression}\n/); } }); diff --git a/packages/svelte/tests/runtime-runes/samples/error-boundary-18/main.svelte b/packages/svelte/tests/runtime-runes/samples/error-boundary-18/main.svelte index b9de7126db..e73dcacc8a 100644 --- a/packages/svelte/tests/runtime-runes/samples/error-boundary-18/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/error-boundary-18/main.svelte @@ -1,16 +1,18 @@ { throw(e) }}> -
Count: {count}
- - {count} / {test} +
Count: {count}
+ + {count} / {maybe_throw()}
diff --git a/packages/svelte/tests/runtime-runes/samples/event-media-element-cleanup/_config.js b/packages/svelte/tests/runtime-runes/samples/event-media-element-cleanup/_config.js new file mode 100644 index 0000000000..775afcab89 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/event-media-element-cleanup/_config.js @@ -0,0 +1,20 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; +import { expect, vi } from 'vitest'; + +const handler = vi.fn(); + +export default test({ + props: { + handler + }, + async test({ target }) { + const button = target.querySelector('button'); + const video = target.querySelector('video'); + + button?.click(); + flushSync(); + video?.dispatchEvent(new Event('someevent')); + expect(handler).not.toHaveBeenCalled(); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/event-media-element-cleanup/main.svelte b/packages/svelte/tests/runtime-runes/samples/event-media-element-cleanup/main.svelte new file mode 100644 index 0000000000..6a1f9b6b4b --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/event-media-element-cleanup/main.svelte @@ -0,0 +1,9 @@ + + + +{#if show} + +{/if} diff --git a/packages/svelte/tests/runtime-runes/samples/flush-sync-no-scheduled/_config.js b/packages/svelte/tests/runtime-runes/samples/flush-sync-no-scheduled/_config.js new file mode 100644 index 0000000000..85e06fa8ec --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/flush-sync-no-scheduled/_config.js @@ -0,0 +1,15 @@ +import { ok, test } from '../../test'; + +export default test({ + async test({ assert, target }) { + const btn = target.querySelector('button'); + const main = target.querySelector('main'); + ok(main); + assert.htmlEqual(main.innerHTML, `
true
`); + // we don't want to use flush sync (or tick that use it inside) since we are testing that calling `flushSync` once + // when there are no scheduled effects does not cause reactivity to break + btn?.click(); + await Promise.resolve(); + assert.htmlEqual(main.innerHTML, `
false
false
`); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/flush-sync-no-scheduled/main.svelte b/packages/svelte/tests/runtime-runes/samples/flush-sync-no-scheduled/main.svelte new file mode 100644 index 0000000000..448d495cf5 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/flush-sync-no-scheduled/main.svelte @@ -0,0 +1,23 @@ + + + + +
+
{flag}
+ + {#if !flag} +
{test}
+ {/if} +
+ diff --git a/packages/svelte/tests/runtime-runes/samples/functional-templating/_config.js b/packages/svelte/tests/runtime-runes/samples/functional-templating/_config.js new file mode 100644 index 0000000000..5d36f2e969 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/functional-templating/_config.js @@ -0,0 +1,9 @@ +import { test } from '../../test'; + +export default test({ + compileOptions: { + fragments: 'tree' + }, + + html: `

hello

` +}); diff --git a/packages/svelte/tests/runtime-runes/samples/functional-templating/main.svelte b/packages/svelte/tests/runtime-runes/samples/functional-templating/main.svelte new file mode 100644 index 0000000000..302a01f335 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/functional-templating/main.svelte @@ -0,0 +1 @@ +

hello

diff --git a/packages/svelte/tests/runtime-runes/samples/if-nested-template/Component.svelte b/packages/svelte/tests/runtime-runes/samples/if-nested-template/Component.svelte new file mode 100644 index 0000000000..b4281bbcbd --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/if-nested-template/Component.svelte @@ -0,0 +1,7 @@ + diff --git a/packages/svelte/tests/runtime-runes/samples/if-nested-template/_config.js b/packages/svelte/tests/runtime-runes/samples/if-nested-template/_config.js new file mode 100644 index 0000000000..673f668916 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/if-nested-template/_config.js @@ -0,0 +1,17 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target, logs }) { + const [btn1, btn2] = target.querySelectorAll('button'); + const [div] = target.querySelectorAll('div'); + + flushSync(() => btn1?.click()); + assert.htmlEqual(div.innerHTML, '123 123'); + assert.equal(div.inert, true); + + flushSync(() => btn2?.click()); + assert.htmlEqual(div.innerHTML, ''); + assert.deepEqual(logs, ['123']); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/if-nested-template/main.svelte b/packages/svelte/tests/runtime-runes/samples/if-nested-template/main.svelte new file mode 100644 index 0000000000..04afa7d664 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/if-nested-template/main.svelte @@ -0,0 +1,23 @@ + + +{#if outer} +
+ {#if inner} + {@const text = inner.toString()} + {text} {inner.toString()} + + {/if} +
+{/if} + + + diff --git a/packages/svelte/tests/runtime-runes/samples/inspect-trace-class/_config.js b/packages/svelte/tests/runtime-runes/samples/inspect-trace-class/_config.js new file mode 100644 index 0000000000..98b3fb6cbc --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/inspect-trace-class/_config.js @@ -0,0 +1,31 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; +import { normalise_trace_logs } from '../../../helpers.js'; + +export default test({ + compileOptions: { + dev: true + }, + + test({ assert, target, logs }) { + assert.deepEqual(normalise_trace_logs(logs), [ + { log: 'effect' }, + { log: '$state', highlighted: true }, + { log: 'Counter.#count', highlighted: false }, + { log: 0 } + ]); + + logs.length = 0; + + const button = target.querySelector('button'); + button?.click(); + flushSync(); + + assert.deepEqual(normalise_trace_logs(logs), [ + { log: 'effect' }, + { log: '$state', highlighted: true }, + { log: 'Counter.#count', highlighted: false }, + { log: 1 } + ]); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/inspect-trace-class/main.svelte b/packages/svelte/tests/runtime-runes/samples/inspect-trace-class/main.svelte new file mode 100644 index 0000000000..56bd497e09 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/inspect-trace-class/main.svelte @@ -0,0 +1,28 @@ + + + diff --git a/packages/svelte/tests/runtime-runes/samples/inspect-trace-each/Entry.svelte b/packages/svelte/tests/runtime-runes/samples/inspect-trace-each/Entry.svelte new file mode 100644 index 0000000000..a22f006dcc --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/inspect-trace-each/Entry.svelte @@ -0,0 +1,8 @@ + diff --git a/packages/svelte/tests/runtime-runes/samples/inspect-trace-each/_config.js b/packages/svelte/tests/runtime-runes/samples/inspect-trace-each/_config.js new file mode 100644 index 0000000000..94cd9d8aaf --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/inspect-trace-each/_config.js @@ -0,0 +1,40 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; +import { normalise_trace_logs } from '../../../helpers.js'; + +export default test({ + compileOptions: { + dev: true + }, + + test({ assert, target, logs }) { + assert.deepEqual(normalise_trace_logs(logs), [ + { log: 'effect' }, + { log: '$state', highlighted: true }, + { log: 'array', highlighted: false }, + { log: [{ id: 1, hi: true }] }, + // this _doesn't_ appear in the browser, but it does appear during tests + // and i cannot for the life of me figure out why. this does at least + // test that we don't log `array[0].id` etc + { log: '$state', highlighted: true }, + { log: 'array[0]', highlighted: false }, + { log: { id: 1, hi: true } } + ]); + + logs.length = 0; + + const button = target.querySelector('button'); + button?.click(); + flushSync(); + + assert.deepEqual(normalise_trace_logs(logs), [ + { log: 'effect' }, + { log: '$state', highlighted: true }, + { log: 'array', highlighted: false }, + { log: [{ id: 1, hi: false }] }, + { log: '$state', highlighted: false }, + { log: 'array[0]', highlighted: false }, + { log: { id: 1, hi: false } } + ]); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/inspect-trace-each/main.svelte b/packages/svelte/tests/runtime-runes/samples/inspect-trace-each/main.svelte new file mode 100644 index 0000000000..e89ee7d9bc --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/inspect-trace-each/main.svelte @@ -0,0 +1,11 @@ + + + + +{#each array as entry (entry.id)} + +{/each} diff --git a/packages/svelte/tests/runtime-runes/samples/inspect-trace-nested/_config.js b/packages/svelte/tests/runtime-runes/samples/inspect-trace-nested/_config.js index f54f78f5c1..5957d2cc6a 100644 --- a/packages/svelte/tests/runtime-runes/samples/inspect-trace-nested/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/inspect-trace-nested/_config.js @@ -1,29 +1,6 @@ import { flushSync } from 'svelte'; import { test } from '../../test'; - -/** - * @param {any[]} logs - */ -function normalise_trace_logs(logs) { - let normalised = []; - for (let i = 0; i < logs.length; i++) { - const log = logs[i]; - - if (typeof log === 'string' && log.includes('%c')) { - const split = log.split('%c'); - normalised.push({ - log: (split[0].length !== 0 ? split[0] : split[1]).trim(), - highlighted: logs[i + 1] === 'color: CornflowerBlue; font-weight: bold' - }); - i++; - } else if (log instanceof Error) { - continue; - } else { - normalised.push({ log }); - } - } - return normalised; -} +import { normalise_trace_logs } from '../../../helpers.js'; export default test({ compileOptions: { @@ -34,10 +11,11 @@ export default test({ // initial log, everything is highlighted assert.deepEqual(normalise_trace_logs(logs), [ - { log: 'iife', highlighted: false }, + { log: 'iife' }, { log: '$state', highlighted: true }, + { log: 'count', highlighted: false }, { log: 0 }, - { log: 'effect', highlighted: false } + { log: 'effect' } ]); logs.length = 0; @@ -47,10 +25,11 @@ export default test({ flushSync(); assert.deepEqual(normalise_trace_logs(logs), [ - { log: 'iife', highlighted: false }, + { log: 'iife' }, { log: '$state', highlighted: true }, + { log: 'count', highlighted: false }, { log: 1 }, - { log: 'effect', highlighted: false } + { log: 'effect' } ]); } }); diff --git a/packages/svelte/tests/runtime-runes/samples/inspect-trace-reassignment/_config.js b/packages/svelte/tests/runtime-runes/samples/inspect-trace-reassignment/_config.js index c9a66289a1..b4f2cf3691 100644 --- a/packages/svelte/tests/runtime-runes/samples/inspect-trace-reassignment/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/inspect-trace-reassignment/_config.js @@ -1,29 +1,6 @@ import { flushSync } from 'svelte'; import { test } from '../../test'; - -/** - * @param {any[]} logs - */ -function normalise_trace_logs(logs) { - let normalised = []; - for (let i = 0; i < logs.length; i++) { - const log = logs[i]; - - if (typeof log === 'string' && log.includes('%c')) { - const split = log.split('%c'); - normalised.push({ - log: (split[0].length !== 0 ? split[0] : split[1]).trim(), - highlighted: logs[i + 1] === 'color: CornflowerBlue; font-weight: bold' - }); - i++; - } else if (log instanceof Error) { - continue; - } else { - normalised.push({ log }); - } - } - return normalised; -} +import { normalise_trace_logs } from '../../../helpers.js'; export default test({ compileOptions: { @@ -34,8 +11,9 @@ export default test({ // initial log, everything is highlighted assert.deepEqual(normalise_trace_logs(logs), [ - { log: 'effect', highlighted: false }, + { log: 'effect' }, { log: '$state', highlighted: true }, + { log: 'checked', highlighted: false }, { log: false } ]); @@ -52,20 +30,26 @@ export default test({ // checked changed, effect reassign state, values should be correct and be correctly highlighted assert.deepEqual(normalise_trace_logs(logs), [ - { log: 'effect', highlighted: false }, + { log: 'effect' }, { log: '$state', highlighted: true }, + { log: 'checked', highlighted: false }, { log: true }, { log: '$state', highlighted: true }, - { log: 1 }, - { log: 'effect', highlighted: false }, + { log: 'count', highlighted: false }, + { log: 2 }, + { log: 'effect' }, { log: '$state', highlighted: false }, + { log: 'checked', highlighted: false }, { log: true }, { log: '$state', highlighted: true }, - { log: 2 }, - { log: 'effect', highlighted: false }, + { log: 'count', highlighted: false }, + { log: 3 }, + { log: 'effect' }, { log: '$state', highlighted: false }, + { log: 'checked', highlighted: false }, { log: true }, { log: '$state', highlighted: true }, + { log: 'count', highlighted: false }, { log: 3 } ]); } diff --git a/packages/svelte/tests/runtime-runes/samples/inspect-trace/_config.js b/packages/svelte/tests/runtime-runes/samples/inspect-trace/_config.js index efa5985e4e..8e9204c90f 100644 --- a/packages/svelte/tests/runtime-runes/samples/inspect-trace/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/inspect-trace/_config.js @@ -1,29 +1,6 @@ import { flushSync } from 'svelte'; import { test } from '../../test'; - -/** - * @param {any[]} logs - */ -function normalise_trace_logs(logs) { - let normalised = []; - for (let i = 0; i < logs.length; i++) { - const log = logs[i]; - - if (typeof log === 'string' && log.includes('%c')) { - const split = log.split('%c'); - normalised.push({ - log: (split[0].length !== 0 ? split[0] : split[1]).trim(), - highlighted: logs[i + 1] === 'color: CornflowerBlue; font-weight: bold' - }); - i++; - } else if (log instanceof Error) { - continue; - } else { - normalised.push({ log }); - } - } - return normalised; -} +import { normalise_trace_logs } from '../../../helpers.js'; export default test({ compileOptions: { @@ -34,12 +11,15 @@ export default test({ // initial log, everything is highlighted assert.deepEqual(normalise_trace_logs(logs), [ - { log: 'effect', highlighted: false }, + { log: 'effect' }, { log: '$derived', highlighted: true }, + { log: 'double', highlighted: false }, { log: 0 }, { log: '$state', highlighted: true }, + { log: 'count', highlighted: false }, { log: 0 }, { log: '$state', highlighted: true }, + { log: 'checked', highlighted: false }, { log: false } ]); @@ -52,12 +32,15 @@ export default test({ // count changed, derived and state are highlighted, last state is not assert.deepEqual(normalise_trace_logs(logs), [ - { log: 'effect', highlighted: false }, + { log: 'effect' }, { log: '$derived', highlighted: true }, + { log: 'double', highlighted: false }, { log: 2 }, { log: '$state', highlighted: true }, + { log: 'count', highlighted: false }, { log: 1 }, { log: '$state', highlighted: false }, + { log: 'checked', highlighted: false }, { log: false } ]); @@ -70,12 +53,15 @@ export default test({ // checked changed, last state is highlighted, first two are not assert.deepEqual(normalise_trace_logs(logs), [ - { log: 'effect', highlighted: false }, + { log: 'effect' }, { log: '$derived', highlighted: false }, + { log: 'double', highlighted: false }, { log: 2 }, { log: '$state', highlighted: false }, + { log: 'count', highlighted: false }, { log: 1 }, { log: '$state', highlighted: true }, + { log: 'checked', highlighted: false }, { log: true } ]); @@ -87,10 +73,12 @@ export default test({ // count change and derived it's >=4, checked is not in the dependencies anymore assert.deepEqual(normalise_trace_logs(logs), [ - { log: 'effect', highlighted: false }, + { log: 'effect' }, { log: '$derived', highlighted: true }, + { log: 'double', highlighted: false }, { log: 4 }, { log: '$state', highlighted: true }, + { log: 'count', highlighted: false }, { log: 2 } ]); } diff --git a/packages/svelte/tests/runtime-runes/samples/media-query/_config.js b/packages/svelte/tests/runtime-runes/samples/media-query/_config.js index f7a4ca05f5..d8b202955a 100644 --- a/packages/svelte/tests/runtime-runes/samples/media-query/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/media-query/_config.js @@ -5,5 +5,10 @@ export default test({ async test({ window }) { expect(window.matchMedia).toHaveBeenCalledWith('(max-width: 599px), (min-width: 900px)'); expect(window.matchMedia).toHaveBeenCalledWith('(min-width: 900px)'); + expect(window.matchMedia).toHaveBeenCalledWith('screen'); + expect(window.matchMedia).toHaveBeenCalledWith('not print'); + expect(window.matchMedia).toHaveBeenCalledWith('screen,print'); + expect(window.matchMedia).toHaveBeenCalledWith('screen, print'); + expect(window.matchMedia).toHaveBeenCalledWith('screen, random'); } }); diff --git a/packages/svelte/tests/runtime-runes/samples/media-query/main.svelte b/packages/svelte/tests/runtime-runes/samples/media-query/main.svelte index 446a9213dd..fe07ed8ab0 100644 --- a/packages/svelte/tests/runtime-runes/samples/media-query/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/media-query/main.svelte @@ -3,4 +3,9 @@ const mq = new MediaQuery("(max-width: 599px), (min-width: 900px)"); const mq2 = new MediaQuery("min-width: 900px"); + const mq3 = new MediaQuery("screen"); + const mq4 = new MediaQuery("not print"); + const mq5 = new MediaQuery("screen,print"); + const mq6 = new MediaQuery("screen, print"); + const mq7 = new MediaQuery("screen, random"); diff --git a/packages/svelte/tests/runtime-runes/samples/non-configurable-errors/_config.js b/packages/svelte/tests/runtime-runes/samples/non-configurable-errors/_config.js new file mode 100644 index 0000000000..5bbe4483d3 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/non-configurable-errors/_config.js @@ -0,0 +1,8 @@ +import { test } from '../../test'; + +export default test({ + compileOptions: { + dev: true + }, + error: 'test' +}); diff --git a/packages/svelte/tests/runtime-runes/samples/non-configurable-errors/main.svelte b/packages/svelte/tests/runtime-runes/samples/non-configurable-errors/main.svelte new file mode 100644 index 0000000000..f71a5e6c43 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/non-configurable-errors/main.svelte @@ -0,0 +1,11 @@ + diff --git a/packages/svelte/tests/runtime-runes/samples/nullish-empty-string/_config.js b/packages/svelte/tests/runtime-runes/samples/nullish-empty-string/_config.js new file mode 100644 index 0000000000..84e97e9735 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/nullish-empty-string/_config.js @@ -0,0 +1,5 @@ +import { test } from '../../test'; + +export default test({ + html: '[]' +}); diff --git a/packages/svelte/tests/runtime-runes/samples/nullish-empty-string/main.svelte b/packages/svelte/tests/runtime-runes/samples/nullish-empty-string/main.svelte new file mode 100644 index 0000000000..efe39b91fe --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/nullish-empty-string/main.svelte @@ -0,0 +1 @@ +[{undefined ?? null}] diff --git a/packages/svelte/tests/runtime-runes/samples/ownership-invalid-mutation-use-transform/_config.js b/packages/svelte/tests/runtime-runes/samples/ownership-invalid-mutation-use-transform/_config.js new file mode 100644 index 0000000000..95556f4737 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/ownership-invalid-mutation-use-transform/_config.js @@ -0,0 +1,10 @@ +import { test } from '../../test'; + +export default test({ + compileOptions: { + dev: true + }, + async test({ assert, errors }) { + assert.deepEqual(errors, []); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/ownership-invalid-mutation-use-transform/main.svelte b/packages/svelte/tests/runtime-runes/samples/ownership-invalid-mutation-use-transform/main.svelte new file mode 100644 index 0000000000..d834551221 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/ownership-invalid-mutation-use-transform/main.svelte @@ -0,0 +1,5 @@ + + diff --git a/packages/svelte/tests/runtime-runes/samples/props-and-slots/Child.svelte b/packages/svelte/tests/runtime-runes/samples/props-and-slots/Child.svelte new file mode 100644 index 0000000000..a2e7d6d8a4 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/props-and-slots/Child.svelte @@ -0,0 +1,9 @@ + + +

{Object.keys(props)}

+ +{#if $$slots.foo} +

foo exists

+{/if} diff --git a/packages/svelte/tests/runtime-runes/samples/props-and-slots/_config.js b/packages/svelte/tests/runtime-runes/samples/props-and-slots/_config.js new file mode 100644 index 0000000000..3f6fb4143c --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/props-and-slots/_config.js @@ -0,0 +1,8 @@ +import { test } from '../../test'; + +export default test({ + html: ` +

a

+

foo exists

+ ` +}); diff --git a/packages/svelte/tests/runtime-runes/samples/props-and-slots/main.svelte b/packages/svelte/tests/runtime-runes/samples/props-and-slots/main.svelte new file mode 100644 index 0000000000..3535da7132 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/props-and-slots/main.svelte @@ -0,0 +1,7 @@ + + + +
foo
+
diff --git a/packages/svelte/tests/runtime-runes/samples/reactive-identifier/_config.js b/packages/svelte/tests/runtime-runes/samples/reactive-identifier/_config.js new file mode 100644 index 0000000000..de59e8c7c7 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/reactive-identifier/_config.js @@ -0,0 +1,13 @@ +import { flushSync } from '../../../../src/index-client'; +import { test } from '../../test'; + +export default test({ + html: ``, + + test({ assert, target }) { + const btn = target.querySelector('button'); + + flushSync(() => btn?.click()); + assert.htmlEqual(target.innerHTML, ``); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/reactive-identifier/main.svelte b/packages/svelte/tests/runtime-runes/samples/reactive-identifier/main.svelte new file mode 100644 index 0000000000..aee0b15e1b --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/reactive-identifier/main.svelte @@ -0,0 +1,13 @@ + + + diff --git a/packages/svelte/tests/runtime-runes/samples/select-multiple-invalid-value/_config.js b/packages/svelte/tests/runtime-runes/samples/select-multiple-invalid-value/_config.js new file mode 100644 index 0000000000..1f04f176af --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/select-multiple-invalid-value/_config.js @@ -0,0 +1,7 @@ +import { test } from '../../test'; + +export default test({ + warnings: [ + 'The `value` property of a ` + + + + diff --git a/packages/svelte/tests/runtime-runes/samples/snippet-destructure-array/_config.js b/packages/svelte/tests/runtime-runes/samples/snippet-destructure-array/_config.js new file mode 100644 index 0000000000..05e3e80b79 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/snippet-destructure-array/_config.js @@ -0,0 +1,5 @@ +import { test } from '../../test'; + +export default test({ + html: `a` +}); diff --git a/packages/svelte/tests/runtime-runes/samples/snippet-destructure-array/main.svelte b/packages/svelte/tests/runtime-runes/samples/snippet-destructure-array/main.svelte new file mode 100644 index 0000000000..37345c629e --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/snippet-destructure-array/main.svelte @@ -0,0 +1,9 @@ + + +{#snippet content([x])} + {x} +{/snippet} + +{@render content(array)} \ No newline at end of file diff --git a/packages/svelte/tests/runtime-runes/samples/spread-element-input-select/_config.js b/packages/svelte/tests/runtime-runes/samples/spread-element-input-select/_config.js new file mode 100644 index 0000000000..71802ba6de --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/spread-element-input-select/_config.js @@ -0,0 +1,20 @@ +import { flushSync } from 'svelte'; +import { ok, test } from '../../test'; + +export default test({ + async test({ assert, target }) { + const select = target.querySelector('select'); + ok(select); + const [option1, option2] = select; + + assert.ok(option1.selected); + assert.ok(!option2.selected); + + const btn = target.querySelector('button'); + flushSync(() => { + btn?.click(); + }); + assert.ok(option1.selected); + assert.ok(!option2.selected); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/spread-element-input-select/main.svelte b/packages/svelte/tests/runtime-runes/samples/spread-element-input-select/main.svelte new file mode 100644 index 0000000000..fb1f7ddf96 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/spread-element-input-select/main.svelte @@ -0,0 +1,10 @@ + + + + + diff --git a/packages/svelte/tests/runtime-runes/samples/svelte-meta-css-wrapper/Component.svelte b/packages/svelte/tests/runtime-runes/samples/svelte-meta-css-wrapper/Component.svelte new file mode 100644 index 0000000000..5668311b0e --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/svelte-meta-css-wrapper/Component.svelte @@ -0,0 +1,7 @@ +

hello from component

+ + diff --git a/packages/svelte/tests/runtime-runes/samples/svelte-meta-css-wrapper/_config.js b/packages/svelte/tests/runtime-runes/samples/svelte-meta-css-wrapper/_config.js new file mode 100644 index 0000000000..1d76b0dd00 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/svelte-meta-css-wrapper/_config.js @@ -0,0 +1,42 @@ +import { test } from '../../test'; + +export default test({ + compileOptions: { + dev: true + }, + + html: ` +

hello

+ +

hello from component

+
+

goodbye

+ `, + + async test({ target, assert }) { + const h1 = target.querySelector('h1'); + const h2 = target.querySelector('h2'); + const p = target.querySelector('p'); + + // @ts-expect-error + assert.deepEqual(h1.__svelte_meta.loc, { + file: 'main.svelte', + line: 5, + column: 0 + }); + + // @ts-expect-error + assert.deepEqual(h2.__svelte_meta.loc, { + file: 'Component.svelte', + line: 1, + column: 0 + }); + + // @ts-expect-error + assert.deepEqual(p.__svelte_meta.loc, { + file: 'main.svelte', + line: 7, + column: 0 + }); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/svelte-meta-css-wrapper/main.svelte b/packages/svelte/tests/runtime-runes/samples/svelte-meta-css-wrapper/main.svelte new file mode 100644 index 0000000000..f49a48fb52 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/svelte-meta-css-wrapper/main.svelte @@ -0,0 +1,7 @@ + + +

hello

+ +

goodbye

diff --git a/packages/svelte/tests/runtime-runes/samples/typescript/main.svelte b/packages/svelte/tests/runtime-runes/samples/typescript/main.svelte index d1b6452df4..4fc7c4ec38 100644 --- a/packages/svelte/tests/runtime-runes/samples/typescript/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/typescript/main.svelte @@ -14,6 +14,7 @@ class Foo { public name: string; + declare bar: string; x = 'x' as const; constructor(name: string) { this.name = name; diff --git a/packages/svelte/tests/runtime-runes/samples/writable-derived-3/_config.js b/packages/svelte/tests/runtime-runes/samples/writable-derived-3/_config.js new file mode 100644 index 0000000000..999e4ad6e0 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/writable-derived-3/_config.js @@ -0,0 +1,5 @@ +import { test } from '../../test'; + +export default test({ + html: `3 3 3 3` +}); diff --git a/packages/svelte/tests/runtime-runes/samples/writable-derived-3/main.svelte b/packages/svelte/tests/runtime-runes/samples/writable-derived-3/main.svelte new file mode 100644 index 0000000000..0b20f811c3 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/writable-derived-3/main.svelte @@ -0,0 +1,29 @@ + + +{x.on_class} {x.in_constructor} {x.on_class_private} {x.in_constructor_private} diff --git a/packages/svelte/tests/server-side-rendering/samples/destructure-state-iterable/_expected.html b/packages/svelte/tests/server-side-rendering/samples/destructure-state-iterable/_expected.html new file mode 100644 index 0000000000..e3f755a08c --- /dev/null +++ b/packages/svelte/tests/server-side-rendering/samples/destructure-state-iterable/_expected.html @@ -0,0 +1 @@ +0, 1 \ No newline at end of file diff --git a/packages/svelte/tests/server-side-rendering/samples/destructure-state-iterable/main.svelte b/packages/svelte/tests/server-side-rendering/samples/destructure-state-iterable/main.svelte new file mode 100644 index 0000000000..9414735f2f --- /dev/null +++ b/packages/svelte/tests/server-side-rendering/samples/destructure-state-iterable/main.svelte @@ -0,0 +1,11 @@ + + +{one}, {two} \ No newline at end of file diff --git a/packages/svelte/tests/server-side-rendering/samples/destructure-state/_expected.html b/packages/svelte/tests/server-side-rendering/samples/destructure-state/_expected.html new file mode 100644 index 0000000000..213a5f5bf1 --- /dev/null +++ b/packages/svelte/tests/server-side-rendering/samples/destructure-state/_expected.html @@ -0,0 +1 @@ +10, Admin \ No newline at end of file diff --git a/packages/svelte/tests/server-side-rendering/samples/destructure-state/main.svelte b/packages/svelte/tests/server-side-rendering/samples/destructure-state/main.svelte new file mode 100644 index 0000000000..422548cd14 --- /dev/null +++ b/packages/svelte/tests/server-side-rendering/samples/destructure-state/main.svelte @@ -0,0 +1,5 @@ + + +{level}, {custom} \ No newline at end of file diff --git a/packages/svelte/tests/server-side-rendering/samples/select-value-bind-store/_expected.html b/packages/svelte/tests/server-side-rendering/samples/select-value-bind-store/_expected.html new file mode 100644 index 0000000000..96d1d8b233 --- /dev/null +++ b/packages/svelte/tests/server-side-rendering/samples/select-value-bind-store/_expected.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/svelte/tests/server-side-rendering/samples/select-value-bind-store/main.svelte b/packages/svelte/tests/server-side-rendering/samples/select-value-bind-store/main.svelte new file mode 100644 index 0000000000..cb3b554762 --- /dev/null +++ b/packages/svelte/tests/server-side-rendering/samples/select-value-bind-store/main.svelte @@ -0,0 +1,10 @@ + + + \ No newline at end of file diff --git a/packages/svelte/tests/server-side-rendering/samples/select-value-component/Option.svelte b/packages/svelte/tests/server-side-rendering/samples/select-value-component/Option.svelte new file mode 100644 index 0000000000..a17413bd8d --- /dev/null +++ b/packages/svelte/tests/server-side-rendering/samples/select-value-component/Option.svelte @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/packages/svelte/tests/server-side-rendering/samples/select-value-component/_expected.html b/packages/svelte/tests/server-side-rendering/samples/select-value-component/_expected.html new file mode 100644 index 0000000000..96d1d8b233 --- /dev/null +++ b/packages/svelte/tests/server-side-rendering/samples/select-value-component/_expected.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/svelte/tests/server-side-rendering/samples/select-value-component/main.svelte b/packages/svelte/tests/server-side-rendering/samples/select-value-component/main.svelte new file mode 100644 index 0000000000..e545833759 --- /dev/null +++ b/packages/svelte/tests/server-side-rendering/samples/select-value-component/main.svelte @@ -0,0 +1,8 @@ + + \ No newline at end of file diff --git a/packages/svelte/tests/server-side-rendering/samples/select-value-implicit-value-complex/_expected.html b/packages/svelte/tests/server-side-rendering/samples/select-value-implicit-value-complex/_expected.html new file mode 100644 index 0000000000..de7cba8837 --- /dev/null +++ b/packages/svelte/tests/server-side-rendering/samples/select-value-implicit-value-complex/_expected.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/svelte/tests/server-side-rendering/samples/select-value-implicit-value-complex/main.svelte b/packages/svelte/tests/server-side-rendering/samples/select-value-implicit-value-complex/main.svelte new file mode 100644 index 0000000000..45ae9aefe0 --- /dev/null +++ b/packages/svelte/tests/server-side-rendering/samples/select-value-implicit-value-complex/main.svelte @@ -0,0 +1,13 @@ + + +{#snippet option(val)}{val}{/snippet} \ No newline at end of file diff --git a/packages/svelte/tests/server-side-rendering/samples/select-value-implicit-value/_expected.html b/packages/svelte/tests/server-side-rendering/samples/select-value-implicit-value/_expected.html new file mode 100644 index 0000000000..de7cba8837 --- /dev/null +++ b/packages/svelte/tests/server-side-rendering/samples/select-value-implicit-value/_expected.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/svelte/tests/server-side-rendering/samples/select-value-implicit-value/main.svelte b/packages/svelte/tests/server-side-rendering/samples/select-value-implicit-value/main.svelte new file mode 100644 index 0000000000..2308a4e7a9 --- /dev/null +++ b/packages/svelte/tests/server-side-rendering/samples/select-value-implicit-value/main.svelte @@ -0,0 +1,5 @@ + \ No newline at end of file diff --git a/packages/svelte/tests/server-side-rendering/samples/select-value/_expected.html b/packages/svelte/tests/server-side-rendering/samples/select-value/_expected.html new file mode 100644 index 0000000000..96d1d8b233 --- /dev/null +++ b/packages/svelte/tests/server-side-rendering/samples/select-value/_expected.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/svelte/tests/server-side-rendering/samples/select-value/main.svelte b/packages/svelte/tests/server-side-rendering/samples/select-value/main.svelte new file mode 100644 index 0000000000..811d752d4e --- /dev/null +++ b/packages/svelte/tests/server-side-rendering/samples/select-value/main.svelte @@ -0,0 +1,5 @@ + \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/await-block-scope/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/await-block-scope/_expected/client/index.svelte.js index 3e5a12ed9d..9bb45ebf78 100644 --- a/packages/svelte/tests/snapshot/samples/await-block-scope/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/await-block-scope/_expected/client/index.svelte.js @@ -5,7 +5,7 @@ function increment(_, counter) { counter.count += 1; } -var root = $.template(` `, 1); +var root = $.from_html(` `, 1); export default function Await_block_scope($$anchor) { let counter = $.proxy({ count: 0 }); diff --git a/packages/svelte/tests/snapshot/samples/bind-component-snippet/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/bind-component-snippet/_expected/client/index.svelte.js index 390e86a351..ba3f4b155a 100644 --- a/packages/svelte/tests/snapshot/samples/bind-component-snippet/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/bind-component-snippet/_expected/client/index.svelte.js @@ -10,7 +10,7 @@ const snippet = ($$anchor) => { $.append($$anchor, text); }; -var root = $.template(` `, 1); +var root = $.from_html(` `, 1); export default function Bind_component_snippet($$anchor) { let value = $.state(''); diff --git a/packages/svelte/tests/snapshot/samples/class-state-field-constructor-assignment/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/class-state-field-constructor-assignment/_expected/client/index.svelte.js index 2133974176..c9725d6718 100644 --- a/packages/svelte/tests/snapshot/samples/class-state-field-constructor-assignment/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/class-state-field-constructor-assignment/_expected/client/index.svelte.js @@ -5,7 +5,7 @@ export default function Class_state_field_constructor_assignment($$anchor, $$pro $.push($$props, true); class Foo { - #a = $.state(); + #a = $.state(0); get a() { return $.get(this.#a); @@ -16,10 +16,31 @@ export default function Class_state_field_constructor_assignment($$anchor, $$pro } #b = $.state(); + #foo = $.derived(() => ({ bar: this.a * 2 })); + + get foo() { + return $.get(this.#foo); + } + + set foo(value) { + $.set(this.#foo, value); + } + + #bar = $.derived(() => ({ baz: this.foo })); + + get bar() { + return $.get(this.#bar); + } + + set bar(value) { + $.set(this.#bar, value); + } constructor() { this.a = 1; $.set(this.#b, 2); + this.foo.bar = 3; + this.bar = 4; } } diff --git a/packages/svelte/tests/snapshot/samples/class-state-field-constructor-assignment/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/class-state-field-constructor-assignment/_expected/server/index.svelte.js index 2a115a4983..abfc264fea 100644 --- a/packages/svelte/tests/snapshot/samples/class-state-field-constructor-assignment/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/class-state-field-constructor-assignment/_expected/server/index.svelte.js @@ -4,12 +4,33 @@ export default function Class_state_field_constructor_assignment($$payload, $$pr $.push(); class Foo { - a; + a = 0; #b; + #foo = $.derived(() => ({ bar: this.a * 2 })); + + get foo() { + return this.#foo(); + } + + set foo($$value) { + return this.#foo($$value); + } + + #bar = $.derived(() => ({ baz: this.foo })); + + get bar() { + return this.#bar(); + } + + set bar($$value) { + return this.#bar($$value); + } constructor() { this.a = 1; this.#b = 2; + this.foo.bar = 3; + this.bar = 4; } } diff --git a/packages/svelte/tests/snapshot/samples/class-state-field-constructor-assignment/index.svelte b/packages/svelte/tests/snapshot/samples/class-state-field-constructor-assignment/index.svelte index a3ff5917e7..127cfd4d2a 100644 --- a/packages/svelte/tests/snapshot/samples/class-state-field-constructor-assignment/index.svelte +++ b/packages/svelte/tests/snapshot/samples/class-state-field-constructor-assignment/index.svelte @@ -1,11 +1,14 @@ diff --git a/packages/svelte/tests/snapshot/samples/destructured-assignments/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/destructured-assignments/_expected/client/index.svelte.js index 47f297bce9..e9cf5b573d 100644 --- a/packages/svelte/tests/snapshot/samples/destructured-assignments/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/destructured-assignments/_expected/client/index.svelte.js @@ -7,10 +7,12 @@ let c = 3; let d = 4; export function update(array) { - ( - $.set(a, array[0], true), - $.set(b, array[1], true) - ); + ((array) => { + var $$array = $.to_array(array, 2); + + $.set(a, $$array[0], true); + $.set(b, $$array[1], true); + })(array); [c, d] = array; } \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/dynamic-attributes-casing/_expected/client/main.svelte.js b/packages/svelte/tests/snapshot/samples/dynamic-attributes-casing/_expected/client/main.svelte.js index 219db6ffd5..28bb01fb18 100644 --- a/packages/svelte/tests/snapshot/samples/dynamic-attributes-casing/_expected/client/main.svelte.js +++ b/packages/svelte/tests/snapshot/samples/dynamic-attributes-casing/_expected/client/main.svelte.js @@ -1,7 +1,7 @@ import 'svelte/internal/disclose-version'; import * as $ from 'svelte/internal/client'; -var root = $.template(`
`, 3); +var root = $.from_html(`
`, 3); export default function Main($$anchor) { // needs to be a snapshot test because jsdom does auto-correct the attribute casing @@ -9,10 +9,16 @@ export default function Main($$anchor) { let y = () => 'test'; var fragment = root(); var div = $.first_child(fragment); + + $.set_attribute(div, 'foobar', x); + var svg = $.sibling(div, 2); + + $.set_attribute(svg, 'viewBox', x); + var custom_element = $.sibling(svg, 2); - $.template_effect(() => $.set_custom_element_data(custom_element, 'fooBar', x)); + $.set_custom_element_data(custom_element, 'fooBar', x); var div_1 = $.sibling(custom_element, 2); var svg_1 = $.sibling(div_1, 2); @@ -22,8 +28,6 @@ export default function Main($$anchor) { $.template_effect( ($0, $1) => { - $.set_attribute(div, 'foobar', x); - $.set_attribute(svg, 'viewBox', x); $.set_attribute(div_1, 'foobar', $0); $.set_attribute(svg_1, 'viewBox', $1); }, diff --git a/packages/svelte/tests/snapshot/samples/each-index-non-null/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/each-index-non-null/_expected/client/index.svelte.js index 3d46a679b8..804a7c26f1 100644 --- a/packages/svelte/tests/snapshot/samples/each-index-non-null/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/each-index-non-null/_expected/client/index.svelte.js @@ -2,7 +2,7 @@ import 'svelte/internal/disclose-version'; import 'svelte/internal/flags/legacy'; import * as $ from 'svelte/internal/client'; -var root_1 = $.template(`

`); +var root_1 = $.from_html(`

`); export default function Each_index_non_null($$anchor) { var fragment = $.comment(); diff --git a/packages/svelte/tests/snapshot/samples/functional-templating/_config.js b/packages/svelte/tests/snapshot/samples/functional-templating/_config.js new file mode 100644 index 0000000000..23231c969b --- /dev/null +++ b/packages/svelte/tests/snapshot/samples/functional-templating/_config.js @@ -0,0 +1,7 @@ +import { test } from '../../test'; + +export default test({ + compileOptions: { + fragments: 'tree' + } +}); diff --git a/packages/svelte/tests/snapshot/samples/functional-templating/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/functional-templating/_expected/client/index.svelte.js new file mode 100644 index 0000000000..792d5421e1 --- /dev/null +++ b/packages/svelte/tests/snapshot/samples/functional-templating/_expected/client/index.svelte.js @@ -0,0 +1,25 @@ +import 'svelte/internal/disclose-version'; +import 'svelte/internal/flags/legacy'; +import * as $ from 'svelte/internal/client'; + +var root = $.from_tree( + [ + ['h1', null, 'hello'], + ' ', + [ + 'div', + { class: 'potato' }, + ['p', null, 'child element'], + ' ', + ['p', null, 'another child element'] + ] + ], + 1 +); + +export default function Functional_templating($$anchor) { + var fragment = root(); + + $.next(2); + $.append($$anchor, fragment); +} \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/functional-templating/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/functional-templating/_expected/server/index.svelte.js new file mode 100644 index 0000000000..dc49c0c213 --- /dev/null +++ b/packages/svelte/tests/snapshot/samples/functional-templating/_expected/server/index.svelte.js @@ -0,0 +1,5 @@ +import * as $ from 'svelte/internal/server'; + +export default function Functional_templating($$payload) { + $$payload.out += `

hello

child element

another child element

`; +} \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/functional-templating/index.svelte b/packages/svelte/tests/snapshot/samples/functional-templating/index.svelte new file mode 100644 index 0000000000..c0fe8965b8 --- /dev/null +++ b/packages/svelte/tests/snapshot/samples/functional-templating/index.svelte @@ -0,0 +1,6 @@ +

hello

+ +
+

child element

+

another child element

+
diff --git a/packages/svelte/tests/snapshot/samples/hello-world/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/hello-world/_expected/client/index.svelte.js index 899c126001..68fdaa4570 100644 --- a/packages/svelte/tests/snapshot/samples/hello-world/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/hello-world/_expected/client/index.svelte.js @@ -2,7 +2,7 @@ import 'svelte/internal/disclose-version'; import 'svelte/internal/flags/legacy'; import * as $ from 'svelte/internal/client'; -var root = $.template(`

hello world

`); +var root = $.from_html(`

hello world

`); export default function Hello_world($$anchor) { var h1 = root(); diff --git a/packages/svelte/tests/snapshot/samples/hmr/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/hmr/_expected/client/index.svelte.js index 3c8322500b..1fac1338c5 100644 --- a/packages/svelte/tests/snapshot/samples/hmr/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/hmr/_expected/client/index.svelte.js @@ -2,7 +2,7 @@ import 'svelte/internal/disclose-version'; import 'svelte/internal/flags/legacy'; import * as $ from 'svelte/internal/client'; -var root = $.template(`

hello world

`); +var root = $.from_html(`

hello world

`); function Hmr($$anchor) { var h1 = root(); diff --git a/packages/svelte/tests/snapshot/samples/nullish-coallescence-omittance/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/nullish-coallescence-omittance/_expected/client/index.svelte.js index 21f6ed9680..b46acee82e 100644 --- a/packages/svelte/tests/snapshot/samples/nullish-coallescence-omittance/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/nullish-coallescence-omittance/_expected/client/index.svelte.js @@ -2,7 +2,7 @@ import 'svelte/internal/disclose-version'; import * as $ from 'svelte/internal/client'; var on_click = (_, count) => $.update(count); -var root = $.template(`

`, 1); +var root = $.from_html(`

`, 1); export default function Nullish_coallescence_omittance($$anchor) { let name = 'world'; diff --git a/packages/svelte/tests/snapshot/samples/purity/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/purity/_expected/client/index.svelte.js index 5bc9766acf..a351851875 100644 --- a/packages/svelte/tests/snapshot/samples/purity/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/purity/_expected/client/index.svelte.js @@ -2,13 +2,13 @@ import 'svelte/internal/disclose-version'; import 'svelte/internal/flags/legacy'; import * as $ from 'svelte/internal/client'; -var root = $.template(`

`, 1); +var root = $.from_html(`

`, 1); export default function Purity($$anchor) { var fragment = root(); var p = $.first_child(fragment); - p.textContent = 0; + p.textContent = '0'; var p_1 = $.sibling(p, 2); diff --git a/packages/svelte/tests/snapshot/samples/skip-static-subtree/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/skip-static-subtree/_expected/client/index.svelte.js index 541b56a407..78147659ff 100644 --- a/packages/svelte/tests/snapshot/samples/skip-static-subtree/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/skip-static-subtree/_expected/client/index.svelte.js @@ -1,7 +1,7 @@ import 'svelte/internal/disclose-version'; import * as $ from 'svelte/internal/client'; -var root = $.template(`

we don't need to traverse these nodes

or

these

ones

these

trailing

nodes

can

be

completely

ignored

`, 3); +var root = $.from_html(`

we don't need to traverse these nodes

or

these

ones

these

trailing

nodes

can

be

completely

ignored

`, 3); export default function Skip_static_subtree($$anchor, $$props) { var fragment = root(); diff --git a/packages/svelte/tests/snapshot/samples/skip-static-subtree/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/skip-static-subtree/_expected/server/index.svelte.js index e694c12647..0532ec5aa9 100644 --- a/packages/svelte/tests/snapshot/samples/skip-static-subtree/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/skip-static-subtree/_expected/server/index.svelte.js @@ -3,5 +3,5 @@ import * as $ from 'svelte/internal/server'; export default function Skip_static_subtree($$payload, $$props) { let { title, content } = $$props; - $$payload.out += `

${$.escape(title)}

we don't need to traverse these nodes

or

these

ones

${$.html(content)}

these

trailing

nodes

can

be

completely

ignored

`; + $$payload.out += `

${$.escape(title)}

we don't need to traverse these nodes

or

these

ones

${$.html(content)}

these

trailing

nodes

can

be

completely

ignored

`; } \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/state-proxy-literal/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/state-proxy-literal/_expected/client/index.svelte.js index a67210e541..c446b3d3ef 100644 --- a/packages/svelte/tests/snapshot/samples/state-proxy-literal/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/state-proxy-literal/_expected/client/index.svelte.js @@ -8,7 +8,7 @@ function reset(_, str, tpl) { $.set(tpl, ``); } -var root = $.template(` `, 1); +var root = $.from_html(` `, 1); export default function State_proxy_literal($$anchor) { let str = $.state(''); diff --git a/packages/svelte/tests/snapshot/samples/state-proxy-literal/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/state-proxy-literal/_expected/server/index.svelte.js index 7b2a884d70..f814dd4f84 100644 --- a/packages/svelte/tests/snapshot/samples/state-proxy-literal/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/state-proxy-literal/_expected/server/index.svelte.js @@ -11,5 +11,5 @@ export default function State_proxy_literal($$payload) { tpl = ``; } - $$payload.out += ` `; + $$payload.out += ` `; } \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/text-nodes-deriveds/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/text-nodes-deriveds/_expected/client/index.svelte.js index d520d1ef24..464435cb0a 100644 --- a/packages/svelte/tests/snapshot/samples/text-nodes-deriveds/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/text-nodes-deriveds/_expected/client/index.svelte.js @@ -1,7 +1,7 @@ import 'svelte/internal/disclose-version'; import * as $ from 'svelte/internal/client'; -var root = $.template(`

`); +var root = $.from_html(`

`); export default function Text_nodes_deriveds($$anchor) { let count1 = 0; diff --git a/packages/svelte/tests/validator/samples/bidirectional-control-characters/input.svelte b/packages/svelte/tests/validator/samples/bidirectional-control-characters/input.svelte new file mode 100644 index 0000000000..21587e5f4f --- /dev/null +++ b/packages/svelte/tests/validator/samples/bidirectional-control-characters/input.svelte @@ -0,0 +1,8 @@ + +⁧⁦def⁩⁦abc⁩⁩ +

Hello, {name}!

+ + +⁧⁦def⁩⁦abc⁩⁩ diff --git a/packages/svelte/tests/validator/samples/bidirectional-control-characters/warnings.json b/packages/svelte/tests/validator/samples/bidirectional-control-characters/warnings.json new file mode 100644 index 0000000000..6e70193c6c --- /dev/null +++ b/packages/svelte/tests/validator/samples/bidirectional-control-characters/warnings.json @@ -0,0 +1,50 @@ +[ + { + "code": "bidirectional_control_characters", + "message": "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", + "start": { + "line": 2, + "column": 15 + }, + "end": { + "line": 2, + "column": 58 + } + }, + { + "code": "bidirectional_control_characters", + "message": "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", + "start": { + "line": 4, + "column": 0 + }, + "end": { + "line": 4, + "column": 2 + } + }, + { + "code": "bidirectional_control_characters", + "message": "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", + "start": { + "line": 4, + "column": 5 + }, + "end": { + "line": 4, + "column": 7 + } + }, + { + "code": "bidirectional_control_characters", + "message": "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", + "start": { + "line": 4, + "column": 10 + }, + "end": { + "line": 4, + "column": 12 + } + } +] diff --git a/packages/svelte/tests/validator/samples/class-state-constructor-1/errors.json b/packages/svelte/tests/validator/samples/class-state-constructor-1/errors.json new file mode 100644 index 0000000000..82765c51c1 --- /dev/null +++ b/packages/svelte/tests/validator/samples/class-state-constructor-1/errors.json @@ -0,0 +1,14 @@ +[ + { + "code": "state_field_duplicate", + "message": "`count` has already been declared on this class", + "start": { + "line": 5, + "column": 2 + }, + "end": { + "line": 5, + "column": 24 + } + } +] diff --git a/packages/svelte/tests/validator/samples/class-state-constructor-1/input.svelte.js b/packages/svelte/tests/validator/samples/class-state-constructor-1/input.svelte.js new file mode 100644 index 0000000000..05cd4d9d9d --- /dev/null +++ b/packages/svelte/tests/validator/samples/class-state-constructor-1/input.svelte.js @@ -0,0 +1,7 @@ +export class Counter { + count = $state(0); + + constructor() { + this.count = $state(0); + } +} diff --git a/packages/svelte/tests/validator/samples/class-state-constructor-10/errors.json b/packages/svelte/tests/validator/samples/class-state-constructor-10/errors.json new file mode 100644 index 0000000000..c4cb0991d0 --- /dev/null +++ b/packages/svelte/tests/validator/samples/class-state-constructor-10/errors.json @@ -0,0 +1,14 @@ +[ + { + "code": "state_field_invalid_assignment", + "message": "Cannot assign to a state field before its declaration", + "start": { + "line": 4, + "column": 3 + }, + "end": { + "line": 4, + "column": 18 + } + } +] diff --git a/packages/svelte/tests/validator/samples/class-state-constructor-10/input.svelte.js b/packages/svelte/tests/validator/samples/class-state-constructor-10/input.svelte.js new file mode 100644 index 0000000000..e5ad562727 --- /dev/null +++ b/packages/svelte/tests/validator/samples/class-state-constructor-10/input.svelte.js @@ -0,0 +1,9 @@ +export class Counter { + constructor() { + if (true) { + this.count = -1; + } + + this.count = $state(0); + } +} diff --git a/packages/svelte/tests/validator/samples/class-state-constructor-2/errors.json b/packages/svelte/tests/validator/samples/class-state-constructor-2/errors.json new file mode 100644 index 0000000000..82765c51c1 --- /dev/null +++ b/packages/svelte/tests/validator/samples/class-state-constructor-2/errors.json @@ -0,0 +1,14 @@ +[ + { + "code": "state_field_duplicate", + "message": "`count` has already been declared on this class", + "start": { + "line": 5, + "column": 2 + }, + "end": { + "line": 5, + "column": 24 + } + } +] diff --git a/packages/svelte/tests/validator/samples/class-state-constructor-2/input.svelte.js b/packages/svelte/tests/validator/samples/class-state-constructor-2/input.svelte.js new file mode 100644 index 0000000000..e37be4b3e6 --- /dev/null +++ b/packages/svelte/tests/validator/samples/class-state-constructor-2/input.svelte.js @@ -0,0 +1,7 @@ +export class Counter { + constructor() { + this.count = $state(0); + this.count = 1; + this.count = $state(0); + } +} diff --git a/packages/svelte/tests/validator/samples/class-state-constructor-3/errors.json b/packages/svelte/tests/validator/samples/class-state-constructor-3/errors.json new file mode 100644 index 0000000000..175c41f98c --- /dev/null +++ b/packages/svelte/tests/validator/samples/class-state-constructor-3/errors.json @@ -0,0 +1,14 @@ +[ + { + "code": "state_field_duplicate", + "message": "`count` has already been declared on this class", + "start": { + "line": 5, + "column": 2 + }, + "end": { + "line": 5, + "column": 28 + } + } +] diff --git a/packages/svelte/tests/validator/samples/class-state-constructor-3/input.svelte.js b/packages/svelte/tests/validator/samples/class-state-constructor-3/input.svelte.js new file mode 100644 index 0000000000..f9196ff3cd --- /dev/null +++ b/packages/svelte/tests/validator/samples/class-state-constructor-3/input.svelte.js @@ -0,0 +1,7 @@ +export class Counter { + constructor() { + this.count = $state(0); + this.count = 1; + this.count = $state.raw(0); + } +} diff --git a/packages/svelte/tests/validator/samples/class-state-constructor-4/errors.json b/packages/svelte/tests/validator/samples/class-state-constructor-4/errors.json new file mode 100644 index 0000000000..9f959874c8 --- /dev/null +++ b/packages/svelte/tests/validator/samples/class-state-constructor-4/errors.json @@ -0,0 +1,14 @@ +[ + { + "code": "state_invalid_placement", + "message": "`$state(...)` 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.", + "start": { + "line": 4, + "column": 16 + }, + "end": { + "line": 4, + "column": 25 + } + } +] diff --git a/packages/svelte/tests/validator/samples/class-state-constructor-4/input.svelte.js b/packages/svelte/tests/validator/samples/class-state-constructor-4/input.svelte.js new file mode 100644 index 0000000000..bf1aada1b5 --- /dev/null +++ b/packages/svelte/tests/validator/samples/class-state-constructor-4/input.svelte.js @@ -0,0 +1,7 @@ +export class Counter { + constructor() { + if (true) { + this.count = $state(0); + } + } +} diff --git a/packages/svelte/tests/validator/samples/class-state-constructor-5/errors.json b/packages/svelte/tests/validator/samples/class-state-constructor-5/errors.json new file mode 100644 index 0000000000..af2f30dade --- /dev/null +++ b/packages/svelte/tests/validator/samples/class-state-constructor-5/errors.json @@ -0,0 +1,14 @@ +[ + { + "code": "state_field_duplicate", + "message": "`count` has already been declared on this class", + "start": { + "line": 5, + "column": 2 + }, + "end": { + "line": 5, + "column": 27 + } + } +] diff --git a/packages/svelte/tests/validator/samples/class-state-constructor-5/input.svelte.js b/packages/svelte/tests/validator/samples/class-state-constructor-5/input.svelte.js new file mode 100644 index 0000000000..bc3d19a14f --- /dev/null +++ b/packages/svelte/tests/validator/samples/class-state-constructor-5/input.svelte.js @@ -0,0 +1,7 @@ +export class Counter { + // prettier-ignore + 'count' = $state(0); + constructor() { + this['count'] = $state(0); + } +} diff --git a/packages/svelte/tests/validator/samples/class-state-constructor-6/errors.json b/packages/svelte/tests/validator/samples/class-state-constructor-6/errors.json new file mode 100644 index 0000000000..ae7a47f31b --- /dev/null +++ b/packages/svelte/tests/validator/samples/class-state-constructor-6/errors.json @@ -0,0 +1,14 @@ +[ + { + "code": "state_field_duplicate", + "message": "`count` has already been declared on this class", + "start": { + "line": 4, + "column": 2 + }, + "end": { + "line": 4, + "column": 27 + } + } +] diff --git a/packages/svelte/tests/validator/samples/class-state-constructor-6/input.svelte.js b/packages/svelte/tests/validator/samples/class-state-constructor-6/input.svelte.js new file mode 100644 index 0000000000..2ebe52e685 --- /dev/null +++ b/packages/svelte/tests/validator/samples/class-state-constructor-6/input.svelte.js @@ -0,0 +1,6 @@ +export class Counter { + count = $state(0); + constructor() { + this['count'] = $state(0); + } +} diff --git a/packages/svelte/tests/validator/samples/class-state-constructor-7/errors.json b/packages/svelte/tests/validator/samples/class-state-constructor-7/errors.json new file mode 100644 index 0000000000..64e56f8d5c --- /dev/null +++ b/packages/svelte/tests/validator/samples/class-state-constructor-7/errors.json @@ -0,0 +1,14 @@ +[ + { + "code": "state_invalid_placement", + "message": "`$state(...)` 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.", + "start": { + "line": 5, + "column": 16 + }, + "end": { + "line": 5, + "column": 25 + } + } +] diff --git a/packages/svelte/tests/validator/samples/class-state-constructor-7/input.svelte.js b/packages/svelte/tests/validator/samples/class-state-constructor-7/input.svelte.js new file mode 100644 index 0000000000..50c8559837 --- /dev/null +++ b/packages/svelte/tests/validator/samples/class-state-constructor-7/input.svelte.js @@ -0,0 +1,7 @@ +const count = 'count'; + +export class Counter { + constructor() { + this[count] = $state(0); + } +} diff --git a/packages/svelte/tests/validator/samples/class-state-constructor-8/errors.json b/packages/svelte/tests/validator/samples/class-state-constructor-8/errors.json new file mode 100644 index 0000000000..2e0bd10ff8 --- /dev/null +++ b/packages/svelte/tests/validator/samples/class-state-constructor-8/errors.json @@ -0,0 +1,14 @@ +[ + { + "code": "state_field_invalid_assignment", + "message": "Cannot assign to a state field before its declaration", + "start": { + "line": 3, + "column": 2 + }, + "end": { + "line": 3, + "column": 17 + } + } +] diff --git a/packages/svelte/tests/validator/samples/class-state-constructor-8/input.svelte.js b/packages/svelte/tests/validator/samples/class-state-constructor-8/input.svelte.js new file mode 100644 index 0000000000..0a76c6fec9 --- /dev/null +++ b/packages/svelte/tests/validator/samples/class-state-constructor-8/input.svelte.js @@ -0,0 +1,6 @@ +export class Counter { + constructor() { + this.count = -1; + this.count = $state(0); + } +} diff --git a/packages/svelte/tests/validator/samples/class-state-constructor-9/errors.json b/packages/svelte/tests/validator/samples/class-state-constructor-9/errors.json new file mode 100644 index 0000000000..b7dd4c8ed4 --- /dev/null +++ b/packages/svelte/tests/validator/samples/class-state-constructor-9/errors.json @@ -0,0 +1,14 @@ +[ + { + "code": "state_field_invalid_assignment", + "message": "Cannot assign to a state field before its declaration", + "start": { + "line": 2, + "column": 1 + }, + "end": { + "line": 2, + "column": 12 + } + } +] diff --git a/packages/svelte/tests/validator/samples/class-state-constructor-9/input.svelte.js b/packages/svelte/tests/validator/samples/class-state-constructor-9/input.svelte.js new file mode 100644 index 0000000000..a8469e13af --- /dev/null +++ b/packages/svelte/tests/validator/samples/class-state-constructor-9/input.svelte.js @@ -0,0 +1,7 @@ +export class Counter { + count = -1; + + constructor() { + this.count = $state(0); + } +} diff --git a/packages/svelte/tests/validator/samples/const-tag-invalid-rune-usage/errors.json b/packages/svelte/tests/validator/samples/const-tag-invalid-rune-usage/errors.json index 32594e4268..e1906b181a 100644 --- a/packages/svelte/tests/validator/samples/const-tag-invalid-rune-usage/errors.json +++ b/packages/svelte/tests/validator/samples/const-tag-invalid-rune-usage/errors.json @@ -1,7 +1,7 @@ [ { "code": "state_invalid_placement", - "message": "`$derived(...)` can only be used as a variable declaration initializer or a class field", + "message": "`$derived(...)` 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.", "start": { "line": 2, "column": 15 diff --git a/packages/svelte/tests/validator/samples/custom-element-props-identifier-props-option/input.svelte b/packages/svelte/tests/validator/samples/custom-element-props-identifier-props-option/input.svelte new file mode 100644 index 0000000000..bb7b930dc3 --- /dev/null +++ b/packages/svelte/tests/validator/samples/custom-element-props-identifier-props-option/input.svelte @@ -0,0 +1,7 @@ + + + \ No newline at end of file diff --git a/packages/svelte/tests/validator/samples/custom-element-props-identifier-props-option/warnings.json b/packages/svelte/tests/validator/samples/custom-element-props-identifier-props-option/warnings.json new file mode 100644 index 0000000000..b880fe146c --- /dev/null +++ b/packages/svelte/tests/validator/samples/custom-element-props-identifier-props-option/warnings.json @@ -0,0 +1,14 @@ +[ + { + "code": "options_missing_custom_element", + "end": { + "column": 2, + "line": 3 + }, + "message": "The `customElement` option is used when generating a custom element. Did you forget the `customElement: true` compile option?", + "start": { + "column": 16, + "line": 1 + } + } +] diff --git a/packages/svelte/tests/validator/samples/custom-element-props-identifier-rest/input.svelte b/packages/svelte/tests/validator/samples/custom-element-props-identifier-rest/input.svelte new file mode 100644 index 0000000000..207b554527 --- /dev/null +++ b/packages/svelte/tests/validator/samples/custom-element-props-identifier-rest/input.svelte @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/packages/svelte/tests/validator/samples/custom-element-props-identifier-rest/warnings.json b/packages/svelte/tests/validator/samples/custom-element-props-identifier-rest/warnings.json new file mode 100644 index 0000000000..61e11ab108 --- /dev/null +++ b/packages/svelte/tests/validator/samples/custom-element-props-identifier-rest/warnings.json @@ -0,0 +1,26 @@ +[ + { + "code": "options_missing_custom_element", + "end": { + "column": 34, + "line": 1 + }, + "message": "The `customElement` option is used when generating a custom element. Did you forget the `customElement: true` compile option?", + "start": { + "column": 16, + "line": 1 + } + }, + { + "code": "custom_element_props_identifier", + "end": { + "column": 15, + "line": 4 + }, + "message": "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.", + "start": { + "column": 7, + "line": 4 + } + } +] diff --git a/packages/svelte/tests/validator/samples/custom-element-props-identifier/input.svelte b/packages/svelte/tests/validator/samples/custom-element-props-identifier/input.svelte new file mode 100644 index 0000000000..ca5b16f8c3 --- /dev/null +++ b/packages/svelte/tests/validator/samples/custom-element-props-identifier/input.svelte @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/packages/svelte/tests/validator/samples/custom-element-props-identifier/warnings.json b/packages/svelte/tests/validator/samples/custom-element-props-identifier/warnings.json new file mode 100644 index 0000000000..4c50bbd116 --- /dev/null +++ b/packages/svelte/tests/validator/samples/custom-element-props-identifier/warnings.json @@ -0,0 +1,26 @@ +[ + { + "code": "options_missing_custom_element", + "end": { + "column": 34, + "line": 1 + }, + "message": "The `customElement` option is used when generating a custom element. Did you forget the `customElement: true` compile option?", + "start": { + "column": 16, + "line": 1 + } + }, + { + "code": "custom_element_props_identifier", + "end": { + "column": 10, + "line": 4 + }, + "message": "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.", + "start": { + "column": 5, + "line": 4 + } + } +] diff --git a/packages/svelte/tests/validator/samples/implicitly-closed-by-parent/input.svelte b/packages/svelte/tests/validator/samples/implicitly-closed-by-parent/input.svelte new file mode 100644 index 0000000000..f67eba18b8 --- /dev/null +++ b/packages/svelte/tests/validator/samples/implicitly-closed-by-parent/input.svelte @@ -0,0 +1,6 @@ +
+ +
+
+

hello

+
diff --git a/packages/svelte/tests/validator/samples/implicitly-closed-by-parent/warnings.json b/packages/svelte/tests/validator/samples/implicitly-closed-by-parent/warnings.json new file mode 100644 index 0000000000..1316a2b65b --- /dev/null +++ b/packages/svelte/tests/validator/samples/implicitly-closed-by-parent/warnings.json @@ -0,0 +1,26 @@ +[ + { + "code": "element_implicitly_closed", + "message": "This element is implicitly closed by the following ``, which can cause an unexpected DOM structure. Add an explicit `
` to avoid surprises.", + "start": { + "line": 1, + "column": 6 + }, + "end": { + "line": 1, + "column": 25 + } + }, + { + "code": "element_implicitly_closed", + "message": "This element is implicitly closed by the following ``, which can cause an unexpected DOM structure. Add an explicit `` to avoid surprises.", + "start": { + "line": 4, + "column": 1 + }, + "end": { + "line": 4, + "column": 20 + } + } +] diff --git a/packages/svelte/tests/validator/samples/implicitly-closed-by-sibling/input.svelte b/packages/svelte/tests/validator/samples/implicitly-closed-by-sibling/input.svelte new file mode 100644 index 0000000000..7721f2f380 --- /dev/null +++ b/packages/svelte/tests/validator/samples/implicitly-closed-by-sibling/input.svelte @@ -0,0 +1,9 @@ +
+

+ +

+
+ +
+

+
diff --git a/packages/svelte/tests/validator/samples/implicitly-closed-by-sibling/warnings.json b/packages/svelte/tests/validator/samples/implicitly-closed-by-sibling/warnings.json new file mode 100644 index 0000000000..6ea36c5a50 --- /dev/null +++ b/packages/svelte/tests/validator/samples/implicitly-closed-by-sibling/warnings.json @@ -0,0 +1,26 @@ +[ + { + "code": "element_implicitly_closed", + "message": "This element is implicitly closed by the following `

`, which can cause an unexpected DOM structure. Add an explicit `

` to avoid surprises.", + "start": { + "line": 2, + "column": 1 + }, + "end": { + "line": 2, + "column": 18 + } + }, + { + "code": "element_implicitly_closed", + "message": "This element is implicitly closed by the following `

`, which can cause an unexpected DOM structure. Add an explicit `

` to avoid surprises.", + "start": { + "line": 8, + "column": 1 + }, + "end": { + "line": 8, + "column": 18 + } + } +] diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index b233cfcc0b..1a83e0d0f1 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -624,6 +624,145 @@ declare module 'svelte/animate' { export {}; } +declare module 'svelte/attachments' { + /** + * 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 { + (element: T): void | (() => void); + } + /** + * 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 + * + * + * + * ``` + * @since 5.29 + */ + export function createAttachmentKey(): symbol; + /** + * 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 + * + *
...
+ * + * + *
bar)}>...
+ * ``` + * */ + export function fromAction(action: Action | ((element: E, arg: T) => void | ActionReturn), fn: () => T): Attachment; + /** + * 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 + * + *
...
+ * + * + *
bar)}>...
+ * ``` + * */ + export function fromAction(action: Action | ((element: E) => void | ActionReturn)): Attachment; + /** + * Actions can return an object containing the two properties defined in this interface. Both are optional. + * - update: An action can have a parameter. This method will be called whenever that parameter changes, + * immediately after Svelte has applied updates to the markup. `ActionReturn` and `ActionReturn` both + * mean that the action accepts no parameters. + * - destroy: Method that is called after the element is unmounted + * + * Additionally, you can specify which additional attributes and events the action enables on the applied element. + * This applies to TypeScript typings only and has no effect at runtime. + * + * Example usage: + * ```ts + * interface Attributes { + * newprop?: string; + * 'on:event': (e: CustomEvent) => void; + * } + * + * export function myAction(node: HTMLElement, parameter: Parameter): ActionReturn { + * // ... + * return { + * update: (updatedParameter) => {...}, + * destroy: () => {...} + * }; + * } + * ``` + */ + interface ActionReturn< + Parameter = undefined, + Attributes extends Record = Record + > { + update?: (parameter: Parameter) => void; + destroy?: () => void; + /** + * ### DO NOT USE THIS + * This exists solely for type-checking and has no effect at runtime. + * Set this through the `Attributes` generic instead. + */ + $$_attributes?: Attributes; + } + + /** + * Actions are functions that are called when an element is created. + * You can use this interface to type such actions. + * The following example defines an action that only works on `
` elements + * and optionally accepts a parameter which it has a default value for: + * ```ts + * export const myAction: Action = (node, param = { someProperty: true }) => { + * // ... + * } + * ``` + * `Action` and `Action` both signal that the action accepts no parameters. + * + * You can return an object with methods `update` and `destroy` from the function and type which additional attributes and events it has. + * See interface `ActionReturn` for more details. + */ + interface Action< + Element = HTMLElement, + Parameter = undefined, + Attributes extends Record = Record + > { + ( + ...args: undefined extends Parameter + ? [node: Node, parameter?: Parameter] + : [node: Node, parameter: Parameter] + ): void | ActionReturn; + } + + // Implementation notes: + // - undefined extends X instead of X extends undefined makes this work better with both strict and nonstrict mode + + export {}; +} + declare module 'svelte/compiler' { import type { SourceMap } from 'magic-string'; import type { ArrayExpression, ArrowFunctionExpression, VariableDeclaration, VariableDeclarator, Expression, Identifier, MemberExpression, Node, ObjectExpression, Pattern, Program, ChainExpression, SimpleCallExpression, SequenceExpression } from 'estree'; @@ -846,6 +985,16 @@ declare module 'svelte/compiler' { * @default false */ preserveWhitespace?: boolean; + /** + * Which strategy to use when cloning DOM fragments: + * + * - `html` populates a `