elements as selected during SSR ([#16017](https://github.com/sveltejs/svelte/pull/16017))
+
+## 5.33.5
+
+### Patch Changes
+
+- fix: handle derived destructured iterators ([#16015](https://github.com/sveltejs/svelte/pull/16015))
+
+- fix: avoid rerunning attachments when unrelated spread attributes change ([#15961](https://github.com/sveltejs/svelte/pull/15961))
+
+## 5.33.4
+
+### Patch Changes
+
+- fix: narrow `defaultChecked` to boolean ([#16009](https://github.com/sveltejs/svelte/pull/16009))
+
+- fix: warn when using rest or identifier in custom elements without props option ([#16003](https://github.com/sveltejs/svelte/pull/16003))
+
+## 5.33.3
+
+### Patch Changes
+
+- fix: allow using typescript in `customElement.extend` option ([#16001](https://github.com/sveltejs/svelte/pull/16001))
+
+- fix: cleanup event handlers on media elements ([#16005](https://github.com/sveltejs/svelte/pull/16005))
+
+## 5.33.2
+
+### Patch Changes
+
+- fix: correctly parse escaped unicode characters in css selector ([#15976](https://github.com/sveltejs/svelte/pull/15976))
+
+- fix: don't mark deriveds as clean if updating during teardown ([#15997](https://github.com/sveltejs/svelte/pull/15997))
+
+## 5.33.1
+
+### Patch Changes
+
+- fix: make deriveds on the server lazy again ([#15964](https://github.com/sveltejs/svelte/pull/15964))
+
+## 5.33.0
+
+### Minor Changes
+
+- feat: XHTML compliance ([#15538](https://github.com/sveltejs/svelte/pull/15538))
+
+- feat: add `fragments: 'html' | 'tree'` option for wider CSP compliance ([#15538](https://github.com/sveltejs/svelte/pull/15538))
+
+## 5.32.2
+
+### Patch Changes
+
+- chore: simplify `` cleaning ([#15980](https://github.com/sveltejs/svelte/pull/15980))
+
+- fix: attach `__svelte_meta` correctly to elements following a CSS wrapper ([#15982](https://github.com/sveltejs/svelte/pull/15982))
+
+- fix: import untrack directly from client in `svelte/attachments` ([#15974](https://github.com/sveltejs/svelte/pull/15974))
+
+## 5.32.1
+
+### Patch Changes
+
+- Warn when an invalid `` value is given ([#14816](https://github.com/sveltejs/svelte/pull/14816))
+
+## 5.32.0
+
+### Minor Changes
+
+- feat: warn on implicitly closed tags ([#15932](https://github.com/sveltejs/svelte/pull/15932))
+
+- feat: attachments `fromAction` utility ([#15933](https://github.com/sveltejs/svelte/pull/15933))
+
+### Patch Changes
+
+- fix: only re-run directly applied attachment if it changed ([#15962](https://github.com/sveltejs/svelte/pull/15962))
+
+## 5.31.1
+
+### Patch Changes
+
+- fix: avoid auto-parenthesis for special-keywords-only `MediaQuery` ([#15937](https://github.com/sveltejs/svelte/pull/15937))
+
+## 5.31.0
+
+### Minor Changes
+
+- feat: allow state fields to be declared inside class constructors ([#15820](https://github.com/sveltejs/svelte/pull/15820))
+
+### Patch Changes
+
+- fix: Add missing `AttachTag` in `Tag` union type inside the `AST` namespace from `"svelte/compiler"` ([#15946](https://github.com/sveltejs/svelte/pull/15946))
+
+## 5.30.2
+
+### Patch Changes
+
+- fix: falsy attachments types ([#15939](https://github.com/sveltejs/svelte/pull/15939))
+
+- fix: handle more hydration mismatches ([#15851](https://github.com/sveltejs/svelte/pull/15851))
+
+## 5.30.1
+
+### Patch Changes
+
+- fix: add `typeParams` to `SnippetBlock` for legacy parser ([#15921](https://github.com/sveltejs/svelte/pull/15921))
+
+## 5.30.0
+
+### Minor Changes
+
+- feat: allow generics on snippets ([#15915](https://github.com/sveltejs/svelte/pull/15915))
+
+## 5.29.0
+
+### Minor Changes
+
+- feat: attachments ([#15000](https://github.com/sveltejs/svelte/pull/15000))
+
+## 5.28.7
+
+### Patch Changes
+
+- fix: remove unncessary guards that require CSP privilege when removing event attributes ([#15846](https://github.com/sveltejs/svelte/pull/15846))
+
+- fix: rewrite destructuring logic to handle iterators ([#15813](https://github.com/sveltejs/svelte/pull/15813))
+
+## 5.28.6
+
+### Patch Changes
+
+- fix: use `transform.read` for `ownership_validator.mutation` array ([#15848](https://github.com/sveltejs/svelte/pull/15848))
+
+- fix: don't redeclare `$slots` ([#15849](https://github.com/sveltejs/svelte/pull/15849))
+
+## 5.28.5
+
+### Patch Changes
+
+- fix: proxify the value in assignment shorthands to the private field ([#15862](https://github.com/sveltejs/svelte/pull/15862))
+
+- fix: more frequently update `bind:buffered` to actual value ([#15874](https://github.com/sveltejs/svelte/pull/15874))
+
+## 5.28.4
+
+### Patch Changes
+
+- fix: treat nullish expression as empty string ([#15901](https://github.com/sveltejs/svelte/pull/15901))
+
+- fix: prevent invalid BigInt calls from blowing up at compile time ([#15900](https://github.com/sveltejs/svelte/pull/15900))
+
+- fix: warn on bidirectional control characters ([#15893](https://github.com/sveltejs/svelte/pull/15893))
+
+- fix: emit right error for a shadowed invalid rune ([#15892](https://github.com/sveltejs/svelte/pull/15892))
+
+## 5.28.3
+
+### Patch Changes
+
+- chore: avoid microtasks when flushing sync ([#15895](https://github.com/sveltejs/svelte/pull/15895))
+
+- fix: improve error message for migration errors when slot would be renamed ([#15841](https://github.com/sveltejs/svelte/pull/15841))
+
+- fix: allow characters in the supplementary special-purpose plane ([#15823](https://github.com/sveltejs/svelte/pull/15823))
+
+## 5.28.2
+
+### Patch Changes
+
+- fix: don't mark selector lists inside `:global` with multiple items as unused ([#15817](https://github.com/sveltejs/svelte/pull/15817))
+
+## 5.28.1
+
+### Patch Changes
+
+- fix: ensure `` properly removes error content in production mode ([#15793](https://github.com/sveltejs/svelte/pull/15793))
+
+- fix: `update_version` after `delete` if `source` is `undefined` and `prop` in `target` ([#15796](https://github.com/sveltejs/svelte/pull/15796))
+
+- fix: emit error on wrong placement of the `:global` block selector ([#15794](https://github.com/sveltejs/svelte/pull/15794))
+
+## 5.28.0
+
+### Minor Changes
+
+- feat: partially evaluate more expressions ([#15781](https://github.com/sveltejs/svelte/pull/15781))
+
+## 5.27.3
+
+### Patch Changes
+
+- fix: use function declaration for snippets in server output to avoid TDZ violation ([#15789](https://github.com/sveltejs/svelte/pull/15789))
+
+## 5.27.2
+
+### Patch Changes
+
+- chore: use pkg.imports for common modules ([#15787](https://github.com/sveltejs/svelte/pull/15787))
+
+## 5.27.1
+
+### Patch Changes
+
+- chore: default params for html blocks ([#15778](https://github.com/sveltejs/svelte/pull/15778))
+
+- fix: correct suggested type for custom events without detail ([#15763](https://github.com/sveltejs/svelte/pull/15763))
+
+- fix: Throw on unrendered snippets in `dev` ([#15766](https://github.com/sveltejs/svelte/pull/15766))
+
+- fix: avoid unnecessary read version increments ([#15777](https://github.com/sveltejs/svelte/pull/15777))
+
+## 5.27.0
+
+### Minor Changes
+
+- feat: partially evaluate certain expressions ([#15494](https://github.com/sveltejs/svelte/pull/15494))
+
+### Patch Changes
+
+- fix: relax `:global` selector list validation ([#15762](https://github.com/sveltejs/svelte/pull/15762))
+
+## 5.26.3
+
+### Patch Changes
+
+- fix: correctly validate head snippets on the server ([#15755](https://github.com/sveltejs/svelte/pull/15755))
+
+- fix: ignore mutation validation for props that are not proxies in more cases ([#15759](https://github.com/sveltejs/svelte/pull/15759))
+
+- fix: allow self-closing tags within math namespace ([#15761](https://github.com/sveltejs/svelte/pull/15761))
+
+## 5.26.2
+
+### Patch Changes
+
+- fix: correctly validate `undefined` snippet params with default value ([#15750](https://github.com/sveltejs/svelte/pull/15750))
+
+## 5.26.1
+
+### Patch Changes
+
+- fix: update `state_referenced_locally` message ([#15733](https://github.com/sveltejs/svelte/pull/15733))
+
+## 5.26.0
+
+### Minor Changes
+
+- feat: add `css.hasGlobal` to `compile` output ([#15450](https://github.com/sveltejs/svelte/pull/15450))
+
+### Patch Changes
+
+- fix: add snippet argument validation in dev ([#15521](https://github.com/sveltejs/svelte/pull/15521))
+
+## 5.25.12
+
+### Patch Changes
+
+- fix: improve internal_set versioning mechanic ([#15724](https://github.com/sveltejs/svelte/pull/15724))
+
+- fix: don't transform reassigned state in labeled statement in `$derived` ([#15725](https://github.com/sveltejs/svelte/pull/15725))
+
+## 5.25.11
+
+### Patch Changes
+
+- fix: handle hydration mismatches in await blocks ([#15708](https://github.com/sveltejs/svelte/pull/15708))
+
+- fix: prevent ownership warnings if the fallback of a bindable is used ([#15720](https://github.com/sveltejs/svelte/pull/15720))
+
+## 5.25.10
+
+### Patch Changes
+
+- fix: set deriveds as `CLEAN` if they are assigned to ([#15592](https://github.com/sveltejs/svelte/pull/15592))
+
+- fix: better scope `:global()` with nesting selector `&` ([#15671](https://github.com/sveltejs/svelte/pull/15671))
+
+## 5.25.9
+
+### Patch Changes
+
+- fix: allow `$.state` and `$.derived` to be treeshaken ([#15702](https://github.com/sveltejs/svelte/pull/15702))
+
+- fix: rework binding ownership validation ([#15678](https://github.com/sveltejs/svelte/pull/15678))
+
+## 5.25.8
+
+### Patch Changes
+
+- fix: address untracked_writes memory leak ([#15694](https://github.com/sveltejs/svelte/pull/15694))
+
+## 5.25.7
+
+### Patch Changes
+
+- fix: ensure clearing of old values happens independent of root flushes ([#15664](https://github.com/sveltejs/svelte/pull/15664))
+
+## 5.25.6
+
+### Patch Changes
+
+- fix: ignore generic type arguments while creating AST ([#15659](https://github.com/sveltejs/svelte/pull/15659))
+
+- fix: better consider component and its snippets during css pruning ([#15630](https://github.com/sveltejs/svelte/pull/15630))
+
+## 5.25.5
+
+### Patch Changes
+
+- fix: add setters to `$derived` class properties ([#15628](https://github.com/sveltejs/svelte/pull/15628))
+
+- fix: silence assignment warning on more function bindings ([#15644](https://github.com/sveltejs/svelte/pull/15644))
+
+- fix: make sure CSS is preserved during SSR with bindings ([#15645](https://github.com/sveltejs/svelte/pull/15645))
+
+## 5.25.4
+
+### Patch Changes
+
+- fix: support TS type assertions ([#15642](https://github.com/sveltejs/svelte/pull/15642))
+
+- fix: ensure `undefined` class still applies scoping class, if necessary ([#15643](https://github.com/sveltejs/svelte/pull/15643))
+
+## 5.25.3
+
+### Patch Changes
+
+- fix: prevent state runes from being called with spread ([#15585](https://github.com/sveltejs/svelte/pull/15585))
+
+## 5.25.2
+
+### Patch Changes
+
+- feat: migrate reassigned deriveds to `$derived` ([#15581](https://github.com/sveltejs/svelte/pull/15581))
+
+## 5.25.1
+
+### Patch Changes
+
+- fix: prevent dev server from throwing errors when attempting to retrieve the proxied value of an iframe's contentWindow ([#15577](https://github.com/sveltejs/svelte/pull/15577))
+
+## 5.25.0
+
+### Minor Changes
+
+- feat: make deriveds writable ([#15570](https://github.com/sveltejs/svelte/pull/15570))
+
+## 5.24.1
+
+### Patch Changes
+
+- fix: use `get` in constructor for deriveds ([#15300](https://github.com/sveltejs/svelte/pull/15300))
+
+- fix: ensure toStore root effect is connected to correct parent effect ([#15574](https://github.com/sveltejs/svelte/pull/15574))
+
+## 5.24.0
+
+### Minor Changes
+
+- feat: allow state created in deriveds/effects to be written/read locally without self-invalidation ([#15553](https://github.com/sveltejs/svelte/pull/15553))
+
+### Patch Changes
+
+- fix: check if DOM prototypes are extensible ([#15569](https://github.com/sveltejs/svelte/pull/15569))
+
+- Keep inlined trailing JSDoc comments of properties when running svelte-migrate ([#15567](https://github.com/sveltejs/svelte/pull/15567))
+
+- fix: simplify set calls for proxyable values ([#15548](https://github.com/sveltejs/svelte/pull/15548))
+
+- fix: don't depend on deriveds created inside the current reaction ([#15564](https://github.com/sveltejs/svelte/pull/15564))
+
+## 5.23.2
+
+### Patch Changes
+
+- fix: don't hoist listeners that access non hoistable snippets ([#15534](https://github.com/sveltejs/svelte/pull/15534))
+
+## 5.23.1
+
+### Patch Changes
+
+- fix: invalidate parent effects when child effects update parent dependencies ([#15506](https://github.com/sveltejs/svelte/pull/15506))
+
+- fix: correctly match `:has()` selector during css pruning ([#15277](https://github.com/sveltejs/svelte/pull/15277))
+
+- fix: replace `undefined` with `void 0` to avoid edge case ([#15511](https://github.com/sveltejs/svelte/pull/15511))
+
+- fix: allow global-like pseudo-selectors refinement ([#15313](https://github.com/sveltejs/svelte/pull/15313))
+
+- chore: don't distribute unused types definitions ([#15473](https://github.com/sveltejs/svelte/pull/15473))
+
+- fix: add `files` and `group` to HTMLInputAttributes in elements.d.ts ([#15492](https://github.com/sveltejs/svelte/pull/15492))
+
+- fix: throw rune_invalid_arguments_length when $state.raw() is used with more than 1 arg ([#15516](https://github.com/sveltejs/svelte/pull/15516))
+
+## 5.23.0
+
+### Minor Changes
+
+- fix: make values consistent between effects and their cleanup functions ([#15469](https://github.com/sveltejs/svelte/pull/15469))
+
+## 5.22.6
+
+### Patch Changes
+
+- fix: skip `log_if_contains_state` if only logging literals ([#15468](https://github.com/sveltejs/svelte/pull/15468))
+
+- fix: Add `closedby` property to HTMLDialogAttributes type ([#15458](https://github.com/sveltejs/svelte/pull/15458))
+
+- fix: null and warnings for local handlers ([#15460](https://github.com/sveltejs/svelte/pull/15460))
+
+## 5.22.5
+
+### Patch Changes
+
+- fix: memoize `clsx` calls ([#15456](https://github.com/sveltejs/svelte/pull/15456))
+
+- fix: respect `svelte-ignore hydration_attribute_changed` on elements with spread attributes ([#15443](https://github.com/sveltejs/svelte/pull/15443))
+
+- fix: always use `setAttribute` when setting `style` ([#15323](https://github.com/sveltejs/svelte/pull/15323))
+
+- fix: make `style:` directive and CSS handling more robust ([#15418](https://github.com/sveltejs/svelte/pull/15418))
+
+## 5.22.4
+
+### Patch Changes
+
+- fix: never deduplicate expressions in templates ([#15451](https://github.com/sveltejs/svelte/pull/15451))
+
+## 5.22.3
+
+### Patch Changes
+
+- fix: run effect roots in tree order ([#15446](https://github.com/sveltejs/svelte/pull/15446))
+
## 5.22.2
### Patch Changes
diff --git a/packages/svelte/elements.d.ts b/packages/svelte/elements.d.ts
index 6d256b56205c..fe4078f56ae1 100644
--- a/packages/svelte/elements.d.ts
+++ b/packages/svelte/elements.d.ts
@@ -31,6 +31,8 @@
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
// TypeScript Version: 2.8
+import type { Attachment } from 'svelte/attachments';
+
// Note: We also allow `null` as a valid value because Svelte treats this the same as `undefined`
type Booleanish = boolean | 'true' | 'false';
@@ -461,6 +463,8 @@ export interface DOMAttributes {
'on:fullscreenerror'?: EventHandler | undefined | null;
onfullscreenerror?: EventHandler | undefined | null;
onfullscreenerrorcapture?: EventHandler | undefined | null;
+
+ xmlns?: string | undefined | null;
}
// All the WAI-ARIA 1.1 attributes from https://www.w3.org/TR/wai-aria-1.1/
@@ -840,6 +844,10 @@ export interface HTMLAttributes extends AriaAttributes, D
readonly 'bind:borderBoxSize'?: Array | undefined | null;
readonly 'bind:devicePixelContentBoxSize'?: Array | undefined | null;
readonly 'bind:focused'?: boolean | undefined | null;
+ readonly 'bind:clientWidth'?: number | undefined | null;
+ readonly 'bind:clientHeight'?: number | undefined | null;
+ readonly 'bind:offsetWidth'?: number | undefined | null;
+ readonly 'bind:offsetHeight'?: number | undefined | null;
// SvelteKit
'data-sveltekit-keepfocus'?: true | '' | 'off' | undefined | null;
@@ -860,6 +868,9 @@ export interface HTMLAttributes extends AriaAttributes, D
// allow any data- attribute
[key: `data-${string}`]: any;
+
+ // allow any attachment and falsy values (by using false we prevent the usage of booleans values by themselves)
+ [key: symbol]: Attachment | false | undefined | null;
}
export type HTMLAttributeAnchorTarget = '_self' | '_blank' | '_parent' | '_top' | (string & {});
@@ -919,6 +930,17 @@ export interface HTMLButtonAttributes extends HTMLAttributes
value?: string | string[] | number | undefined | null;
popovertarget?: string | undefined | null;
popovertargetaction?: 'toggle' | 'show' | 'hide' | undefined | null;
+ command?:
+ | 'show-modal'
+ | 'close'
+ | 'request-close'
+ | 'show-popover'
+ | 'hide-popover'
+ | 'toggle-popover'
+ | (string & {})
+ | undefined
+ | null;
+ commandfor?: string | undefined | null;
}
export interface HTMLCanvasAttributes extends HTMLAttributes {
@@ -957,6 +979,7 @@ export interface HTMLDelAttributes extends HTMLAttributes {
export interface HTMLDialogAttributes extends HTMLAttributes {
open?: boolean | undefined | null;
+ closedby?: 'any' | 'closerequest' | 'none' | undefined | null;
}
export interface HTMLEmbedAttributes extends HTMLAttributes {
@@ -1075,6 +1098,7 @@ export interface HTMLInputAttributes extends HTMLAttributes {
checked?: boolean | undefined | null;
dirname?: string | undefined | null;
disabled?: boolean | undefined | null;
+ files?: FileList | undefined | null;
form?: string | undefined | null;
formaction?: string | undefined | null;
formenctype?:
@@ -1086,6 +1110,7 @@ export interface HTMLInputAttributes extends HTMLAttributes {
formmethod?: 'dialog' | 'get' | 'post' | 'DIALOG' | 'GET' | 'POST' | undefined | null;
formnovalidate?: boolean | undefined | null;
formtarget?: string | undefined | null;
+ group?: any | undefined | null;
height?: number | string | undefined | null;
indeterminate?: boolean | undefined | null;
list?: string | undefined | null;
@@ -1107,8 +1132,8 @@ export interface HTMLInputAttributes extends HTMLAttributes {
// needs both casing variants because language tools does lowercase names of non-shorthand attributes
defaultValue?: any;
defaultvalue?: any;
- defaultChecked?: any;
- defaultchecked?: any;
+ defaultChecked?: boolean | undefined | null;
+ defaultchecked?: boolean | undefined | null;
width?: number | string | undefined | null;
webkitdirectory?: boolean | undefined | null;
@@ -1801,7 +1826,6 @@ export interface SVGAttributes extends AriaAttributes, DO
'xlink:type'?: string | undefined | null;
'xml:base'?: string | undefined | null;
'xml:lang'?: string | undefined | null;
- xmlns?: string | undefined | null;
'xmlns:xlink'?: string | undefined | null;
'xml:space'?: string | undefined | null;
y1?: number | string | undefined | null;
diff --git a/packages/svelte/messages/client-errors/errors.md b/packages/svelte/messages/client-errors/errors.md
index ce1f222c63ea..47c2038d70f8 100644
--- a/packages/svelte/messages/client-errors/errors.md
+++ b/packages/svelte/messages/client-errors/errors.md
@@ -12,7 +12,7 @@
## component_api_changed
-> %parent% called `%method%` on an instance of %component%, which is no longer valid in Svelte 5
+> Calling `%method%` on a component instance (of %component%) is no longer valid in Svelte 5
See the [migration guide](/docs/svelte/v5-migration-guide#Components-are-no-longer-classes) for more information.
@@ -48,6 +48,10 @@ See the [migration guide](/docs/svelte/v5-migration-guide#Components-are-no-long
> Maximum update depth exceeded. This can happen when a reactive block or effect repeatedly sets a new value. Svelte limits the number of nested updates to prevent infinite loops
+## get_abort_signal_outside_reaction
+
+> `getAbortSignal()` can only be called inside an effect or derived
+
## hydration_failed
> Failed to hydrate the application
@@ -80,10 +84,37 @@ See the [migration guide](/docs/svelte/v5-migration-guide#Components-are-no-long
> Cannot set prototype of `$state` object
-## state_unsafe_local_read
+## state_unsafe_mutation
+
+> Updating state inside `$derived(...)`, `$inspect(...)` or a template expression is forbidden. If the value should not be reactive, declare it without `$state`
-> Reading state that was created inside the same derived is forbidden. Consider using `untrack` to read locally created state
+This error occurs when state is updated while evaluating a `$derived`. You might encounter it while trying to 'derive' two pieces of state in one go:
-## state_unsafe_mutation
+```svelte
+
+
+ count++}>{count}
+
+{count} is even: {even}
+{count} is odd: {odd}
+```
+
+This is forbidden because it introduces instability: if `{count} is even: {even}
` is updated before `odd` is recalculated, `even` will be stale. In most cases the solution is to make everything derived:
+
+```js
+let count = 0;
+// ---cut---
+let even = $derived(count % 2 === 0);
+let odd = $derived(!even);
+```
-> Updating state inside a derived or a template expression is forbidden. If the value should not be reactive, declare it without `$state`
+If side-effects are unavoidable, use [`$effect`]($effect) instead.
diff --git a/packages/svelte/messages/client-warnings/warnings.md b/packages/svelte/messages/client-warnings/warnings.md
index 943cf6f01f4f..f1901271d11c 100644
--- a/packages/svelte/messages/client-warnings/warnings.md
+++ b/packages/svelte/messages/client-warnings/warnings.md
@@ -132,7 +132,7 @@ During development, this error is often preceeded by a `console.error` detailing
## ownership_invalid_binding
-> %parent% passed a value to %child% with `bind:`, but the value is owned by %owner%. Consider creating a binding between %owner% and %parent%
+> %parent% passed property `%prop%` to %child% with `bind:`, but its parent component %owner% did not declare `%prop%` as a binding. Consider creating a binding between %owner% and %parent% (e.g. `bind:%prop%={...}` instead of `%prop%={...}`)
Consider three components `GrandParent`, `Parent` and `Child`. If you do ``, inside `GrandParent` pass on the variable via ` ` (note the missing `bind:`) and then do `` inside `Parent`, this warning is thrown.
@@ -140,9 +140,7 @@ To fix it, `bind:` to the value instead of just passing a property (i.e. in this
## ownership_invalid_mutation
-> Mutating a value outside the component that created it is strongly discouraged. Consider passing values to child components with `bind:`, or use a callback instead
-
-> %component% mutated a value owned by %owner%. This is strongly discouraged. Consider passing values to child components with `bind:`, or use a callback instead
+> Mutating unbound props (`%name%`, at %location%) is strongly discouraged. Consider using `bind:%prop%={...}` in %parent% (or using a callback) instead
Consider the following code:
@@ -170,6 +168,17 @@ Consider the following code:
To fix it, either create callback props to communicate changes, or mark `person` as [`$bindable`]($bindable).
+## select_multiple_invalid_value
+
+> The `value` property of a `` element should be an array, but it received a non-array value. The selection will be kept as is.
+
+When using ``, Svelte will mark all selected `` elements as selected by iterating over the array passed to `value`. If `value` is not an array, Svelte will emit this warning and keep the selected options as they are.
+
+To silence the warning, ensure that `value`:
+
+- is an array for an explicit selection
+- is `null` or `undefined` to keep the selection as is
+
## state_proxy_equality_mismatch
> Reactive `$state(...)` proxies and the values they proxy have different identities. Because of this, comparisons with `%operator%` will produce unexpected results
diff --git a/packages/svelte/messages/compile-errors/script.md b/packages/svelte/messages/compile-errors/script.md
index 795c0b007dca..e11975aef26a 100644
--- a/packages/svelte/messages/compile-errors/script.md
+++ b/packages/svelte/messages/compile-errors/script.md
@@ -162,6 +162,10 @@ This turned out to be buggy and unpredictable, particularly when working with de
> `%name%` is not a valid rune
+## rune_invalid_spread
+
+> `%rune%` cannot be called with a spread argument
+
## rune_invalid_usage
> Cannot use `%rune%` rune in non-runes mode
@@ -208,13 +212,41 @@ It's possible to export a snippet from a `
+
+{children}
+```
+
+...or like this (a parent component is passing a snippet where a non-snippet value is expected):
+
+```svelte
+
+
+ {#snippet label()}
+ Hi!
+ {/snippet}
+
+```
+
+```svelte
+
+
+
+
+{label}
+```
+
## store_invalid_shape
> `%name%` is not a store with a `subscribe` method
diff --git a/packages/svelte/package.json b/packages/svelte/package.json
index 478668a602b5..872c21a1794e 100644
--- a/packages/svelte/package.json
+++ b/packages/svelte/package.json
@@ -2,18 +2,19 @@
"name": "svelte",
"description": "Cybernetically enhanced web apps",
"license": "MIT",
- "version": "5.22.2",
+ "version": "5.35.2",
"type": "module",
"types": "./types/index.d.ts",
"engines": {
"node": ">=18"
},
"files": [
+ "*.d.ts",
"src",
"!src/**/*.test.*",
+ "!src/**/*.d.ts",
"types",
"compiler",
- "*.d.ts",
"README.md"
],
"module": "src/index-client.js",
@@ -33,6 +34,10 @@
"types": "./types/index.d.ts",
"default": "./src/animate/index.js"
},
+ "./attachments": {
+ "types": "./types/index.d.ts",
+ "default": "./src/attachments/index.js"
+ },
"./compiler": {
"types": "./types/index.d.ts",
"require": "./compiler/index.js",
@@ -102,6 +107,17 @@
"default": "./src/events/index.js"
}
},
+ "imports": {
+ "#client": "./src/internal/client/types.d.ts",
+ "#client/constants": "./src/internal/client/constants.js",
+ "#compiler": {
+ "types": "./src/compiler/private.d.ts",
+ "default": "./src/compiler/index.js"
+ },
+ "#compiler/builders": "./src/compiler/utils/builders.js",
+ "#server": "./src/internal/server/types.d.ts",
+ "#shared": "./src/internal/shared/types.d.ts"
+ },
"repository": {
"type": "git",
"url": "git+https://github.com/sveltejs/svelte.git",
@@ -120,7 +136,7 @@
],
"scripts": {
"build": "node scripts/process-messages && rollup -c && pnpm generate:types && node scripts/check-treeshakeability.js",
- "dev": "node scripts/process-messages && rollup -cw",
+ "dev": "node scripts/process-messages -w & rollup -cw",
"check": "tsc --project tsconfig.runtime.json && tsc && cd ./tests/types && tsc",
"check:watch": "tsc --watch",
"generate:version": "node ./scripts/generate-version.js",
@@ -148,14 +164,14 @@
"dependencies": {
"@ampproject/remapping": "^2.3.0",
"@jridgewell/sourcemap-codec": "^1.5.0",
+ "@sveltejs/acorn-typescript": "^1.0.5",
"@types/estree": "^1.0.5",
"acorn": "^8.12.1",
- "@sveltejs/acorn-typescript": "^1.0.5",
"aria-query": "^5.3.1",
"axobject-query": "^4.1.0",
"clsx": "^2.1.1",
"esm-env": "^1.2.1",
- "esrap": "^1.4.3",
+ "esrap": "^2.1.0",
"is-reference": "^3.0.3",
"locate-character": "^3.0.0",
"magic-string": "^0.30.11",
diff --git a/packages/svelte/scripts/check-treeshakeability.js b/packages/svelte/scripts/check-treeshakeability.js
index 1501ee69546d..e883496fe2a7 100644
--- a/packages/svelte/scripts/check-treeshakeability.js
+++ b/packages/svelte/scripts/check-treeshakeability.js
@@ -118,36 +118,40 @@ const bundle = await bundle_code(
).js.code
);
-if (!bundle.includes('hydrate_node') && !bundle.includes('hydrate_next')) {
- // eslint-disable-next-line no-console
- console.error(`✅ Hydration code treeshakeable`);
-} else {
- failed = true;
- // eslint-disable-next-line no-console
- console.error(`❌ Hydration code not treeshakeable`);
-}
+/**
+ * @param {string} case_name
+ * @param {string[]} strings
+ */
+function check_bundle(case_name, ...strings) {
+ for (const string of strings) {
+ const index = bundle.indexOf(string);
+ if (index >= 0) {
+ // eslint-disable-next-line no-console
+ console.error(`❌ ${case_name} not treeshakeable`);
+ failed = true;
-if (!bundle.includes('component_context.l')) {
- // eslint-disable-next-line no-console
- console.error(`✅ Legacy code treeshakeable`);
-} else {
- failed = true;
+ let lines = bundle.slice(index - 500, index + 500).split('\n');
+ const target_line = lines.findIndex((line) => line.includes(string));
+ // mark the failed line
+ lines = lines
+ .map((line, i) => (i === target_line ? `> ${line}` : `| ${line}`))
+ .slice(target_line - 5, target_line + 6);
+ // eslint-disable-next-line no-console
+ console.error('The first failed line:\n' + lines.join('\n'));
+ return;
+ }
+ }
// eslint-disable-next-line no-console
- console.error(`❌ Legacy code not treeshakeable`);
+ console.error(`✅ ${case_name} treeshakeable`);
}
-if (!bundle.includes(`'CreatedAt'`)) {
- // eslint-disable-next-line no-console
- console.error(`✅ $inspect.trace code treeshakeable`);
-} else {
- failed = true;
- // eslint-disable-next-line no-console
- console.error(`❌ $inspect.trace code not treeshakeable`);
-}
+check_bundle('Hydration code', 'hydrate_node', 'hydrate_next');
+check_bundle('Legacy code', 'component_context.l');
+check_bundle('$inspect.trace', `'CreatedAt'`);
if (failed) {
// eslint-disable-next-line no-console
- console.error(bundle);
+ console.error('Full bundle at', path.resolve('scripts/_bundle.js'));
fs.writeFileSync('scripts/_bundle.js', bundle);
}
diff --git a/packages/svelte/scripts/generate-types.js b/packages/svelte/scripts/generate-types.js
index d44afe8205a8..c558a2bbf78a 100644
--- a/packages/svelte/scripts/generate-types.js
+++ b/packages/svelte/scripts/generate-types.js
@@ -24,12 +24,18 @@ await createBundle({
output: `${dir}/types/index.d.ts`,
compilerOptions: {
// so that types/properties with `@internal` (and its dependencies) are removed from the output
- stripInternal: true
+ stripInternal: true,
+ paths: Object.fromEntries(
+ Object.entries(pkg.imports).map(([key, value]) => {
+ return [key, [value.types ?? value.default ?? value]];
+ })
+ )
},
modules: {
[pkg.name]: `${dir}/src/index.d.ts`,
[`${pkg.name}/action`]: `${dir}/src/action/public.d.ts`,
[`${pkg.name}/animate`]: `${dir}/src/animate/public.d.ts`,
+ [`${pkg.name}/attachments`]: `${dir}/src/attachments/public.d.ts`,
[`${pkg.name}/compiler`]: `${dir}/src/compiler/public.d.ts`,
[`${pkg.name}/easing`]: `${dir}/src/easing/index.js`,
[`${pkg.name}/legacy`]: `${dir}/src/legacy/legacy-client.js`,
diff --git a/packages/svelte/scripts/process-messages/index.js b/packages/svelte/scripts/process-messages/index.js
index 80619acfa7eb..e705a1c921a1 100644
--- a/packages/svelte/scripts/process-messages/index.js
+++ b/packages/svelte/scripts/process-messages/index.js
@@ -1,274 +1,355 @@
+/** @import { Node } from 'esrap/languages/ts' */
+/** @import * as ESTree from 'estree' */
+/** @import { AST } from 'svelte/compiler' */
+
// @ts-check
+import process from 'node:process';
import fs from 'node:fs';
import * as acorn from 'acorn';
import { walk } from 'zimmerframe';
import * as esrap from 'esrap';
-
-/** @type {Record>} */
-const messages = {};
-const seen = new Set();
+import ts from 'esrap/languages/ts';
const DIR = '../../documentation/docs/98-reference/.generated';
-fs.rmSync(DIR, { force: true, recursive: true });
-fs.mkdirSync(DIR);
-for (const category of fs.readdirSync('messages')) {
- if (category.startsWith('.')) continue;
+const watch = process.argv.includes('-w');
- messages[category] = {};
+function run() {
+ /** @type {Record>} */
+ const messages = {};
+ const seen = new Set();
- for (const file of fs.readdirSync(`messages/${category}`)) {
- if (!file.endsWith('.md')) continue;
+ fs.rmSync(DIR, { force: true, recursive: true });
+ fs.mkdirSync(DIR);
- const markdown = fs
- .readFileSync(`messages/${category}/${file}`, 'utf-8')
- .replace(/\r\n/g, '\n');
+ for (const category of fs.readdirSync('messages')) {
+ if (category.startsWith('.')) continue;
- const sorted = [];
+ messages[category] = {};
- for (const match of markdown.matchAll(/## ([\w]+)\n\n([^]+?)(?=$|\n\n## )/g)) {
- const [_, code, text] = match;
+ for (const file of fs.readdirSync(`messages/${category}`)) {
+ if (!file.endsWith('.md')) continue;
- if (seen.has(code)) {
- throw new Error(`Duplicate message code ${category}/${code}`);
- }
+ const markdown = fs
+ .readFileSync(`messages/${category}/${file}`, 'utf-8')
+ .replace(/\r\n/g, '\n');
- sorted.push({ code, _ });
+ const sorted = [];
- const sections = text.trim().split('\n\n');
- const details = [];
+ for (const match of markdown.matchAll(/## ([\w]+)\n\n([^]+?)(?=$|\n\n## )/g)) {
+ const [_, code, text] = match;
- while (!sections[sections.length - 1].startsWith('> ')) {
- details.unshift(/** @type {string} */ (sections.pop()));
- }
+ if (seen.has(code)) {
+ throw new Error(`Duplicate message code ${category}/${code}`);
+ }
+
+ sorted.push({ code, _ });
+
+ const sections = text.trim().split('\n\n');
+ const details = [];
- if (sections.length === 0) {
- throw new Error('No message text');
+ while (!sections[sections.length - 1].startsWith('> ')) {
+ details.unshift(/** @type {string} */ (sections.pop()));
+ }
+
+ if (sections.length === 0) {
+ throw new Error('No message text');
+ }
+
+ seen.add(code);
+ messages[category][code] = {
+ messages: sections.map((section) => section.replace(/^> /gm, '').replace(/^>\n/gm, '\n')),
+ details: details.join('\n\n')
+ };
}
- seen.add(code);
- messages[category][code] = {
- messages: sections.map((section) => section.replace(/^> /gm, '').replace(/^>\n/gm, '\n')),
- details: details.join('\n\n')
- };
+ sorted.sort((a, b) => (a.code < b.code ? -1 : 1));
+
+ fs.writeFileSync(
+ `messages/${category}/${file}`,
+ sorted.map((x) => x._.trim()).join('\n\n') + '\n'
+ );
}
- sorted.sort((a, b) => (a.code < b.code ? -1 : 1));
fs.writeFileSync(
- `messages/${category}/${file}`,
- sorted.map((x) => x._.trim()).join('\n\n') + '\n'
+ `${DIR}/${category}.md`,
+ '\n\n' +
+ Object.entries(messages[category])
+ .map(([code, { messages, details }]) => {
+ const chunks = [
+ `### ${code}`,
+ ...messages.map((message) => '```\n' + message + '\n```')
+ ];
+
+ if (details) {
+ chunks.push(details);
+ }
+
+ return chunks.join('\n\n');
+ })
+ .sort()
+ .join('\n\n') +
+ '\n'
);
}
- fs.writeFileSync(
- `${DIR}/${category}.md`,
- '\n\n' +
- Object.entries(messages[category])
- .map(([code, { messages, details }]) => {
- const chunks = [`### ${code}`, ...messages.map((message) => '```\n' + message + '\n```')];
+ /**
+ * @param {string} name
+ * @param {string} dest
+ */
+ function transform(name, dest) {
+ const source = fs
+ .readFileSync(new URL(`./templates/${name}.js`, import.meta.url), 'utf-8')
+ .replace(/\r\n/g, '\n');
- if (details) {
- chunks.push(details);
- }
+ /** @type {AST.JSComment[]} */
+ const comments = [];
- return chunks.join('\n\n');
+ let ast = /** @type {ESTree.Node} */ (
+ /** @type {unknown} */ (
+ acorn.parse(source, {
+ ecmaVersion: 'latest',
+ sourceType: 'module',
+ locations: true,
+ onComment: comments
})
- .sort()
- .join('\n\n') +
- '\n'
- );
-}
+ )
+ );
-/**
- * @param {string} name
- * @param {string} dest
- */
-function transform(name, dest) {
- const source = fs
- .readFileSync(new URL(`./templates/${name}.js`, import.meta.url), 'utf-8')
- .replace(/\r\n/g, '\n');
+ comments.forEach((comment) => {
+ if (comment.type === 'Block') {
+ comment.value = comment.value.replace(/^\t+/gm, '');
+ }
+ });
- /**
- * @type {Array<{
- * type: string;
- * value: string;
- * start: number;
- * end: number
- * }>}
- */
- const comments = [];
+ ast = walk(ast, null, {
+ Identifier(node, context) {
+ if (node.name === 'CODES') {
+ /** @type {ESTree.ArrayExpression} */
+ const array = {
+ type: 'ArrayExpression',
+ elements: Object.keys(messages[name]).map((code) => ({
+ type: 'Literal',
+ value: code
+ }))
+ };
- let ast = acorn.parse(source, {
- ecmaVersion: 'latest',
- sourceType: 'module',
- onComment: (block, value, start, end) => {
- if (block && /\n/.test(value)) {
- let a = start;
- while (a > 0 && source[a - 1] !== '\n') a -= 1;
+ return array;
+ }
+ }
+ });
- let b = a;
- while (/[ \t]/.test(source[b])) b += 1;
+ const body = /** @type {ESTree.Program} */ (ast).body;
- const indentation = source.slice(a, b);
- value = value.replace(new RegExp(`^${indentation}`, 'gm'), '');
+ const category = messages[name];
+
+ // find the `export function CODE` node
+ const index = body.findIndex((node) => {
+ if (
+ node.type === 'ExportNamedDeclaration' &&
+ node.declaration &&
+ node.declaration.type === 'FunctionDeclaration'
+ ) {
+ return node.declaration.id.name === 'CODE';
}
+ });
- comments.push({ type: block ? 'Block' : 'Line', value, start, end });
- }
- });
+ if (index === -1) throw new Error(`missing export function CODE in ${name}.js`);
- ast = walk(ast, null, {
- _(node, { next }) {
- let comment;
+ const template_node = body[index];
+ body.splice(index, 1);
- while (comments[0] && comments[0].start < node.start) {
- comment = comments.shift();
- // @ts-expect-error
- (node.leadingComments ||= []).push(comment);
- }
+ const jsdoc = /** @type {AST.JSComment} */ (
+ comments.findLast((comment) => comment.start < /** @type {number} */ (template_node.start))
+ );
- next();
+ const printed = esrap.print(
+ /** @type {Node} */ (ast),
+ ts({
+ comments: comments.filter((comment) => comment !== jsdoc)
+ })
+ );
- if (comments[0]) {
- const slice = source.slice(node.end, comments[0].start);
+ for (const code in category) {
+ const { messages } = category[code];
+ /** @type {string[]} */
+ const vars = [];
- if (/^[,) \t]*$/.test(slice)) {
- // @ts-expect-error
- node.trailingComments = [comments.shift()];
+ const group = messages.map((text, i) => {
+ for (const match of text.matchAll(/%(\w+)%/g)) {
+ const name = match[1];
+ if (!vars.includes(name)) {
+ vars.push(match[1]);
+ }
}
- }
- },
- // @ts-expect-error
- Identifier(node, context) {
- if (node.name === 'CODES') {
+
return {
- type: 'ArrayExpression',
- elements: Object.keys(messages[name]).map((code) => ({
- type: 'Literal',
- value: code
- }))
+ text,
+ vars: vars.slice()
};
- }
- }
- });
+ });
- if (comments.length > 0) {
- // @ts-expect-error
- (ast.trailingComments ||= []).push(...comments);
- }
+ /** @type {ESTree.Expression} */
+ let message = { type: 'Literal', value: '' };
+ let prev_vars;
- const category = messages[name];
+ for (let i = 0; i < group.length; i += 1) {
+ const { text, vars } = group[i];
- // find the `export function CODE` node
- const index = ast.body.findIndex((node) => {
- if (
- node.type === 'ExportNamedDeclaration' &&
- node.declaration &&
- node.declaration.type === 'FunctionDeclaration'
- ) {
- return node.declaration.id.name === 'CODE';
- }
- });
+ if (vars.length === 0) {
+ message = {
+ type: 'Literal',
+ value: text
+ };
+ prev_vars = vars;
+ continue;
+ }
- if (index === -1) throw new Error(`missing export function CODE in ${name}.js`);
+ const parts = text.split(/(%\w+%)/);
- const template_node = ast.body[index];
- ast.body.splice(index, 1);
+ /** @type {ESTree.Expression[]} */
+ const expressions = [];
- for (const code in category) {
- const { messages } = category[code];
- /** @type {string[]} */
- const vars = [];
+ /** @type {ESTree.TemplateElement[]} */
+ const quasis = [];
- const group = messages.map((text, i) => {
- for (const match of text.matchAll(/%(\w+)%/g)) {
- const name = match[1];
- if (!vars.includes(name)) {
- vars.push(match[1]);
+ for (let i = 0; i < parts.length; i += 1) {
+ const part = parts[i];
+ if (i % 2 === 0) {
+ const str = part.replace(/(`|\${)/g, '\\$1');
+ quasis.push({
+ type: 'TemplateElement',
+ value: { raw: str, cooked: str },
+ tail: i === parts.length - 1
+ });
+ } else {
+ expressions.push({
+ type: 'Identifier',
+ name: part.slice(1, -1)
+ });
+ }
}
- }
-
- return {
- text,
- vars: vars.slice()
- };
- });
-
- /** @type {import('estree').Expression} */
- let message = { type: 'Literal', value: '' };
- let prev_vars;
- for (let i = 0; i < group.length; i += 1) {
- const { text, vars } = group[i];
-
- if (vars.length === 0) {
- message = {
- type: 'Literal',
- value: text
+ /** @type {ESTree.Expression} */
+ const expression = {
+ type: 'TemplateLiteral',
+ expressions,
+ quasis
};
- prev_vars = vars;
- continue;
- }
- const parts = text.split(/(%\w+%)/);
-
- /** @type {import('estree').Expression[]} */
- const expressions = [];
-
- /** @type {import('estree').TemplateElement[]} */
- const quasis = [];
+ if (prev_vars) {
+ if (vars.length === prev_vars.length) {
+ throw new Error('Message overloads must have new parameters');
+ }
- for (let i = 0; i < parts.length; i += 1) {
- const part = parts[i];
- if (i % 2 === 0) {
- const str = part.replace(/(`|\${)/g, '\\$1');
- quasis.push({
- type: 'TemplateElement',
- value: { raw: str, cooked: str },
- tail: i === parts.length - 1
- });
+ message = {
+ type: 'ConditionalExpression',
+ test: {
+ type: 'Identifier',
+ name: vars[prev_vars.length]
+ },
+ consequent: expression,
+ alternate: message
+ };
} else {
- expressions.push({
- type: 'Identifier',
- name: part.slice(1, -1)
- });
+ message = expression;
}
+
+ prev_vars = vars;
}
- /** @type {import('estree').Expression} */
- const expression = {
- type: 'TemplateLiteral',
- expressions,
- quasis
- };
+ const clone = /** @type {ESTree.Statement} */ (
+ walk(/** @type {ESTree.Node} */ (template_node), null, {
+ FunctionDeclaration(node, context) {
+ if (node.id.name !== 'CODE') return;
- if (prev_vars) {
- if (vars.length === prev_vars.length) {
- throw new Error('Message overloads must have new parameters');
- }
+ const params = [];
- message = {
- type: 'ConditionalExpression',
- test: {
- type: 'Identifier',
- name: vars[prev_vars.length]
- },
- consequent: expression,
- alternate: message
- };
- } else {
- message = expression;
- }
+ for (const param of node.params) {
+ if (param.type === 'Identifier' && param.name === 'PARAMETER') {
+ params.push(...vars.map((name) => ({ type: 'Identifier', name })));
+ } else {
+ params.push(param);
+ }
+ }
- prev_vars = vars;
- }
+ return /** @type {ESTree.FunctionDeclaration} */ ({
+ .../** @type {ESTree.FunctionDeclaration} */ (context.next()),
+ params,
+ id: {
+ ...node.id,
+ name: code
+ }
+ });
+ },
+ TemplateLiteral(node, context) {
+ /** @type {ESTree.TemplateElement} */
+ let quasi = {
+ type: 'TemplateElement',
+ value: {
+ ...node.quasis[0].value
+ },
+ tail: node.quasis[0].tail
+ };
+
+ /** @type {ESTree.TemplateLiteral} */
+ let out = {
+ type: 'TemplateLiteral',
+ quasis: [quasi],
+ expressions: []
+ };
+
+ for (let i = 0; i < node.expressions.length; i += 1) {
+ const q = structuredClone(node.quasis[i + 1]);
+ const e = node.expressions[i];
+
+ if (e.type === 'Literal' && e.value === 'CODE') {
+ quasi.value.raw += code + q.value.raw;
+ continue;
+ }
+
+ if (e.type === 'Identifier' && e.name === 'MESSAGE') {
+ if (message.type === 'Literal') {
+ const str = /** @type {string} */ (message.value).replace(/(`|\${)/g, '\\$1');
+ quasi.value.raw += str + q.value.raw;
+ continue;
+ }
+
+ if (message.type === 'TemplateLiteral') {
+ const m = structuredClone(message);
+ quasi.value.raw += m.quasis[0].value.raw;
+ out.quasis.push(...m.quasis.slice(1));
+ out.expressions.push(...m.expressions);
+ quasi = m.quasis[m.quasis.length - 1];
+ quasi.value.raw += q.value.raw;
+ continue;
+ }
+ }
+
+ out.quasis.push((quasi = q));
+ out.expressions.push(/** @type {ESTree.Expression} */ (context.visit(e)));
+ }
- const clone = walk(/** @type {import('estree').Node} */ (template_node), null, {
- // @ts-expect-error Block is a block comment, which is not recognised
- Block(node, context) {
- if (!node.value.includes('PARAMETER')) return;
+ return out;
+ },
+ Literal(node) {
+ if (node.value === 'CODE') {
+ return {
+ type: 'Literal',
+ value: code
+ };
+ }
+ },
+ Identifier(node) {
+ if (node.name !== 'MESSAGE') return;
+ return message;
+ }
+ })
+ );
- const value = /** @type {string} */ (node.value)
+ const jsdoc_clone = {
+ ...jsdoc,
+ value: /** @type {string} */ (jsdoc.value)
.split('\n')
.map((line) => {
if (line === ' * MESSAGE') {
@@ -293,117 +374,56 @@ function transform(name, dest) {
return line;
})
.filter((x) => x !== '')
- .join('\n');
-
- if (value !== node.value) {
- return { ...node, value };
- }
- },
- FunctionDeclaration(node, context) {
- if (node.id.name !== 'CODE') return;
-
- const params = [];
-
- for (const param of node.params) {
- if (param.type === 'Identifier' && param.name === 'PARAMETER') {
- params.push(...vars.map((name) => ({ type: 'Identifier', name })));
- } else {
- params.push(param);
- }
- }
-
- return /** @type {import('estree').FunctionDeclaration} */ ({
- .../** @type {import('estree').FunctionDeclaration} */ (context.next()),
- params,
- id: {
- ...node.id,
- name: code
- }
- });
- },
- TemplateLiteral(node, context) {
- /** @type {import('estree').TemplateElement} */
- let quasi = {
- type: 'TemplateElement',
- value: {
- ...node.quasis[0].value
- },
- tail: node.quasis[0].tail
- };
-
- /** @type {import('estree').TemplateLiteral} */
- let out = {
- type: 'TemplateLiteral',
- quasis: [quasi],
- expressions: []
- };
+ .join('\n')
+ };
- for (let i = 0; i < node.expressions.length; i += 1) {
- const q = structuredClone(node.quasis[i + 1]);
- const e = node.expressions[i];
+ const block = esrap.print(
+ // @ts-expect-error some bullshit
+ /** @type {ESTree.Program} */ ({ ...ast, body: [clone] }),
+ ts({ comments: [jsdoc_clone] })
+ ).code;
- if (e.type === 'Literal' && e.value === 'CODE') {
- quasi.value.raw += code + q.value.raw;
- continue;
- }
+ printed.code += `\n\n${block}`;
- if (e.type === 'Identifier' && e.name === 'MESSAGE') {
- if (message.type === 'Literal') {
- const str = /** @type {string} */ (message.value).replace(/(`|\${)/g, '\\$1');
- quasi.value.raw += str + q.value.raw;
- continue;
- }
-
- if (message.type === 'TemplateLiteral') {
- const m = structuredClone(message);
- quasi.value.raw += m.quasis[0].value.raw;
- out.quasis.push(...m.quasis.slice(1));
- out.expressions.push(...m.expressions);
- quasi = m.quasis[m.quasis.length - 1];
- quasi.value.raw += q.value.raw;
- continue;
- }
- }
-
- out.quasis.push((quasi = q));
- out.expressions.push(/** @type {import('estree').Expression} */ (context.visit(e)));
- }
-
- return out;
- },
- Literal(node) {
- if (node.value === 'CODE') {
- return {
- type: 'Literal',
- value: code
- };
- }
- },
- Identifier(node) {
- if (node.name !== 'MESSAGE') return;
- return message;
- }
- });
+ body.push(clone);
+ }
- // @ts-expect-error
- ast.body.push(clone);
+ fs.writeFileSync(
+ dest,
+ `/* This file is generated by scripts/process-messages/index.js. Do not edit! */\n\n` +
+ printed.code,
+ 'utf-8'
+ );
}
- const module = esrap.print(ast);
+ transform('compile-errors', 'src/compiler/errors.js');
+ transform('compile-warnings', 'src/compiler/warnings.js');
- fs.writeFileSync(
- dest,
- `/* This file is generated by scripts/process-messages/index.js. Do not edit! */\n\n` +
- module.code,
- 'utf-8'
- );
+ transform('client-warnings', 'src/internal/client/warnings.js');
+ transform('client-errors', 'src/internal/client/errors.js');
+ transform('server-errors', 'src/internal/server/errors.js');
+ transform('shared-errors', 'src/internal/shared/errors.js');
+ transform('shared-warnings', 'src/internal/shared/warnings.js');
}
-transform('compile-errors', 'src/compiler/errors.js');
-transform('compile-warnings', 'src/compiler/warnings.js');
+if (watch) {
+ let running = false;
+ let timeout;
+
+ fs.watch('messages', { recursive: true }, (type, file) => {
+ if (running) {
+ timeout ??= setTimeout(() => {
+ running = false;
+ timeout = null;
+ });
+ } else {
+ running = true;
+
+ // eslint-disable-next-line no-console
+ console.log('Regenerating messages...');
+ run();
+ }
+ });
+}
-transform('client-warnings', 'src/internal/client/warnings.js');
-transform('client-errors', 'src/internal/client/errors.js');
-transform('server-errors', 'src/internal/server/errors.js');
-transform('shared-errors', 'src/internal/shared/errors.js');
-transform('shared-warnings', 'src/internal/shared/warnings.js');
+run();
diff --git a/packages/svelte/src/attachments/index.js b/packages/svelte/src/attachments/index.js
new file mode 100644
index 000000000000..b9fde9b6d918
--- /dev/null
+++ b/packages/svelte/src/attachments/index.js
@@ -0,0 +1,113 @@
+/** @import { Action, ActionReturn } from '../action/public' */
+/** @import { Attachment } from './public' */
+import { noop, render_effect } from 'svelte/internal/client';
+import { ATTACHMENT_KEY } from '../constants.js';
+import { untrack } from '../index-client.js';
+import { teardown } from '../internal/client/reactivity/effects.js';
+
+/**
+ * Creates an object key that will be recognised as an attachment when the object is spread onto an element,
+ * as a programmatic alternative to using `{@attach ...}`. This can be useful for library authors, though
+ * is generally not needed when building an app.
+ *
+ * ```svelte
+ *
+ *
+ * click me
+ * ```
+ * @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 000000000000..caf1342d0a09
--- /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 677b99fcff81..e9fa33b054cb 100644
--- a/packages/svelte/src/compiler/errors.js
+++ b/packages/svelte/src/compiler/errors.js
@@ -15,10 +15,12 @@ class InternalCompileError extends Error {
constructor(code, message, position) {
super(message);
this.stack = ''; // avoid unnecessary noise; don't set it as a class property or it becomes enumerable
+
// We want to extend from Error so that various bundler plugins properly handle it.
// But we also want to share the same object shape with that of warnings, therefore
// we create an instance of the shared class an copy over its properties.
this.#diagnostic = new CompileDiagnostic(code, message, position);
+
Object.assign(this, this.#diagnostic);
this.name = 'CompileError';
}
@@ -383,6 +385,16 @@ export function rune_invalid_name(node, name) {
e(node, 'rune_invalid_name', `\`${name}\` is not a valid rune\nhttps://svelte.dev/e/rune_invalid_name`);
}
+/**
+ * `%rune%` cannot be called with a spread argument
+ * @param {null | number | NodeLike} node
+ * @param {string} rune
+ * @returns {never}
+ */
+export function rune_invalid_spread(node, rune) {
+ e(node, 'rune_invalid_spread', `\`${rune}\` cannot be called with a spread argument\nhttps://svelte.dev/e/rune_invalid_spread`);
+}
+
/**
* Cannot use `%rune%` rune in non-runes mode
* @param {null | number | NodeLike} node
@@ -451,6 +463,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
@@ -461,13 +492,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`);
}
/**
@@ -545,12 +576,12 @@ export function css_global_block_invalid_declaration(node) {
}
/**
- * A `:global` selector cannot be part of a selector list with more than one item
+ * A `:global` selector cannot be part of a selector list with entries that don't contain `:global`
* @param {null | number | NodeLike} node
* @returns {never}
*/
export function css_global_block_invalid_list(node) {
- e(node, 'css_global_block_invalid_list', `A \`:global\` selector cannot be part of a selector list with more than one item\nhttps://svelte.dev/e/css_global_block_invalid_list`);
+ e(node, 'css_global_block_invalid_list', `A \`:global\` selector cannot be part of a selector list with entries that don't contain \`:global\`\nhttps://svelte.dev/e/css_global_block_invalid_list`);
}
/**
@@ -571,6 +602,15 @@ export function css_global_block_invalid_modifier_start(node) {
e(node, 'css_global_block_invalid_modifier_start', `A \`:global\` selector can only be modified if it is a descendant of other selectors\nhttps://svelte.dev/e/css_global_block_invalid_modifier_start`);
}
+/**
+ * A `:global` selector cannot be inside a pseudoclass
+ * @param {null | number | NodeLike} node
+ * @returns {never}
+ */
+export function css_global_block_invalid_placement(node) {
+ e(node, 'css_global_block_invalid_placement', `A \`:global\` selector cannot be inside a pseudoclass\nhttps://svelte.dev/e/css_global_block_invalid_placement`);
+}
+
/**
* `:global(...)` can be at the start or end of a selector sequence, but not in the middle
* @param {null | number | NodeLike} node
@@ -778,7 +818,9 @@ export function bind_invalid_expression(node) {
* @returns {never}
*/
export function bind_invalid_name(node, name, explanation) {
- e(node, 'bind_invalid_name', `${explanation ? `\`bind:${name}\` is not a valid binding. ${explanation}` : `\`bind:${name}\` is not a valid binding`}\nhttps://svelte.dev/e/bind_invalid_name`);
+ e(node, 'bind_invalid_name', `${explanation
+ ? `\`bind:${name}\` is not a valid binding. ${explanation}`
+ : `\`bind:${name}\` is not a valid binding`}\nhttps://svelte.dev/e/bind_invalid_name`);
}
/**
diff --git a/packages/svelte/src/compiler/index.js b/packages/svelte/src/compiler/index.js
index 42427dd9c407..9ba23c148595 100644
--- a/packages/svelte/src/compiler/index.js
+++ b/packages/svelte/src/compiler/index.js
@@ -3,7 +3,6 @@
/** @import { AST } from './public.js' */
import { walk as zimmerframe_walk } from 'zimmerframe';
import { convert } from './legacy.js';
-import { parse as parse_acorn } from './phases/1-parse/acorn.js';
import { parse as _parse } from './phases/1-parse/index.js';
import { remove_typescript_nodes } from './phases/1-parse/remove_typescript_nodes.js';
import { analyze_component, analyze_module } from './phases/2-analyze/index.js';
@@ -21,9 +20,8 @@ export { default as preprocess } from './preprocess/index.js';
*/
export function compile(source, options) {
source = remove_bom(source);
- state.reset_warning_filter(options.warningFilter);
+ state.reset_warnings(options.warningFilter);
const validated = validate_component_options(options, '');
- state.reset(source, validated);
let parsed = _parse(source);
@@ -43,6 +41,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);
@@ -60,11 +63,10 @@ export function compile(source, options) {
*/
export function compileModule(source, options) {
source = remove_bom(source);
- state.reset_warning_filter(options.warningFilter);
+ state.reset_warnings(options.warningFilter);
const validated = validate_module_options(options, '');
- state.reset(source, validated);
- const analysis = analyze_module(parse_acorn(source, false), validated);
+ const analysis = analyze_module(source, validated);
return transform_module(analysis, source, validated);
}
@@ -92,6 +94,7 @@ export function compileModule(source, options) {
* @returns {Record}
*/
+// TODO 6.0 remove unused `filename`
/**
* The parse function parses a component, returning only its abstract syntax tree.
*
@@ -100,14 +103,15 @@ export function compileModule(source, options) {
*
* The `loose` option, available since 5.13.0, tries to always return an AST even if the input will not successfully compile.
*
+ * The `filename` option is unused and will be removed in Svelte 6.0.
+ *
* @param {string} source
* @param {{ filename?: string; rootDir?: string; modern?: boolean; loose?: boolean }} [options]
* @returns {AST.Root | LegacyRoot}
*/
-export function parse(source, { filename, rootDir, modern, loose } = {}) {
+export function parse(source, { modern, loose } = {}) {
source = remove_bom(source);
- state.reset_warning_filter(() => false);
- state.reset(source, { filename: filename ?? '(unknown)', rootDir });
+ state.reset_warnings(() => false);
const ast = _parse(source, loose);
return to_public_ast(source, ast, modern);
diff --git a/packages/svelte/src/compiler/legacy.js b/packages/svelte/src/compiler/legacy.js
index e3f88c8f1d23..85345bca4a22 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
@@ -450,6 +451,7 @@ export function convert(source, ast) {
SpreadAttribute(node) {
return { ...node, type: 'Spread' };
},
+ // @ts-ignore
StyleSheet(node, context) {
return {
...node,
diff --git a/packages/svelte/src/compiler/migrate/index.js b/packages/svelte/src/compiler/migrate/index.js
index 1bb7a69a20f9..fdc9734f858f 100644
--- a/packages/svelte/src/compiler/migrate/index.js
+++ b/packages/svelte/src/compiler/migrate/index.js
@@ -1,7 +1,7 @@
/** @import { VariableDeclarator, Node, Identifier, AssignmentExpression, LabeledStatement, ExpressionStatement } from 'estree' */
/** @import { Visitors } from 'zimmerframe' */
/** @import { ComponentAnalysis } from '../phases/types.js' */
-/** @import { Scope, ScopeRoot } from '../phases/scope.js' */
+/** @import { Scope } from '../phases/scope.js' */
/** @import { AST, Binding, ValidatedCompileOptions } from '#compiler' */
import MagicString from 'magic-string';
import { walk } from 'zimmerframe';
@@ -9,7 +9,7 @@ import { parse } from '../phases/1-parse/index.js';
import { regex_valid_component_name } from '../phases/1-parse/state/element.js';
import { analyze_component } from '../phases/2-analyze/index.js';
import { get_rune } from '../phases/scope.js';
-import { reset, reset_warning_filter } from '../state.js';
+import { reset, reset_warnings } from '../state.js';
import {
extract_identifiers,
extract_all_identifiers_from_expression,
@@ -134,8 +134,7 @@ export function migrate(source, { filename, use_ts } = {}) {
return start + style_placeholder + end;
});
- reset_warning_filter(() => false);
- reset(source, { filename: filename ?? '(unknown)' });
+ reset_warnings(() => false);
let parsed = parse(source);
@@ -603,15 +602,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,
@@ -944,54 +943,53 @@ const instance_script = {
node.body.type === 'ExpressionStatement' &&
node.body.expression.type === 'AssignmentExpression'
) {
- const ids = extract_identifiers(node.body.expression.left);
- const [, expression_ids] = extract_all_identifiers_from_expression(
- node.body.expression.right
- );
- const bindings = ids.map((id) => state.scope.get(id.name));
- const reassigned_bindings = bindings.filter((b) => b?.reassigned);
+ const { left, right } = node.body.expression;
- if (
- reassigned_bindings.length === 0 &&
- !bindings.some((b) => b?.kind === 'store_sub') &&
- node.body.expression.left.type !== 'MemberExpression'
- ) {
- let { start, end } = /** @type {{ start: number, end: number }} */ (
- node.body.expression.right
- );
+ const ids = extract_identifiers(left);
+ const [, expression_ids] = extract_all_identifiers_from_expression(right);
+ const bindings = ids.map((id) => /** @type {Binding} */ (state.scope.get(id.name)));
- check_rune_binding('derived');
+ if (bindings.every((b) => b.kind === 'legacy_reactive')) {
+ if (
+ right.type !== 'Literal' &&
+ bindings.every((b) => b.kind !== 'store_sub') &&
+ left.type !== 'MemberExpression'
+ ) {
+ let { start, end } = /** @type {{ start: number, end: number }} */ (right);
- // $derived
- state.str.update(
- /** @type {number} */ (node.start),
- /** @type {number} */ (node.body.expression.start),
- 'let '
- );
+ check_rune_binding('derived');
- if (node.body.expression.right.type === 'SequenceExpression') {
- while (state.str.original[start] !== '(') start -= 1;
- while (state.str.original[end - 1] !== ')') end += 1;
- }
+ // $derived
+ state.str.update(
+ /** @type {number} */ (node.start),
+ /** @type {number} */ (node.body.expression.start),
+ 'let '
+ );
- state.str.prependRight(start, `$derived(`);
+ if (right.type === 'SequenceExpression') {
+ while (state.str.original[start] !== '(') start -= 1;
+ while (state.str.original[end - 1] !== ')') end += 1;
+ }
+
+ state.str.prependRight(start, `$derived(`);
+
+ // in a case like `$: ({ a } = b())`, there's already a trailing parenthesis.
+ // otherwise, we need to add one
+ if (state.str.original[/** @type {number} */ (node.body.start)] !== '(') {
+ state.str.appendLeft(end, `)`);
+ }
- // in a case like `$: ({ a } = b())`, there's already a trailing parenthesis.
- // otherwise, we need to add one
- if (state.str.original[/** @type {number} */ (node.body.start)] !== '(') {
- state.str.appendLeft(end, `)`);
+ return;
}
- return;
- } else {
- for (const binding of reassigned_bindings) {
- if (binding && (ids.includes(binding.node) || expression_ids.length === 0)) {
+ for (const binding of bindings) {
+ if (binding.reassigned && (ids.includes(binding.node) || expression_ids.length === 0)) {
check_rune_binding('state');
const init =
binding.kind === 'state'
? ' = $state()'
: expression_ids.length === 0
- ? ` = $state(${state.str.original.substring(/** @type {number} */ (node.body.expression.right.start), node.body.expression.right.end)})`
+ ? ` = $state(${state.str.original.substring(/** @type {number} */ (right.start), right.end)})`
: '';
// implicitly-declared variable which we need to make explicit
state.str.prependLeft(
@@ -1000,7 +998,8 @@ const instance_script = {
);
}
}
- if (expression_ids.length === 0 && !bindings.some((b) => b?.kind === 'store_sub')) {
+
+ if (expression_ids.length === 0 && bindings.every((b) => b.kind !== 'store_sub')) {
state.str.remove(/** @type {number} */ (node.start), /** @type {number} */ (node.end));
return;
}
@@ -1307,7 +1306,7 @@ const template = {
name = state.scope.generate(slot_name);
if (name !== slot_name) {
throw new MigrationError(
- 'This migration would change the name of a slot making the component unusable'
+ `This migration would change the name of a slot (${slot_name} to ${name}) making the component unusable`
);
}
}
@@ -1592,7 +1591,6 @@ function extract_type_and_comment(declarator, state, path) {
const comment_start = /** @type {any} */ (comment_node)?.start;
const comment_end = /** @type {any} */ (comment_node)?.end;
let comment = comment_node && str.original.substring(comment_start, comment_end);
-
if (comment_node) {
str.update(comment_start, comment_end, '');
}
@@ -1673,6 +1671,11 @@ function extract_type_and_comment(declarator, state, path) {
state.has_type_or_fallback = true;
const match = /@type {(.+)}/.exec(comment_node.value);
if (match) {
+ // try to find JSDoc comments after a hyphen `-`
+ const jsdoc_comment = /@type {.+} (?:\w+|\[.*?\]) - (.+)/.exec(comment_node.value);
+ if (jsdoc_comment) {
+ cleaned_comment += jsdoc_comment[1]?.trim();
+ }
return {
type: match[1],
comment: cleaned_comment,
@@ -1693,7 +1696,6 @@ function extract_type_and_comment(declarator, state, path) {
};
}
}
-
return {
type: 'any',
comment: state.uses_ts ? comment : cleaned_comment,
@@ -1877,7 +1879,7 @@ function handle_identifier(node, state, path) {
let new_name = state.scope.generate(name);
if (new_name !== name) {
throw new MigrationError(
- 'This migration would change the name of a slot making the component unusable'
+ `This migration would change the name of a slot (${name} to ${new_name}) making the component unusable`
);
}
}
diff --git a/packages/svelte/src/compiler/phases/1-parse/acorn.js b/packages/svelte/src/compiler/phases/1-parse/acorn.js
index 36f7688c49a3..77ce4a461c2c 100644
--- a/packages/svelte/src/compiler/phases/1-parse/acorn.js
+++ b/packages/svelte/src/compiler/phases/1-parse/acorn.js
@@ -1,18 +1,32 @@
/** @import { Comment, Program } from 'estree' */
+/** @import { AST } from '#compiler' */
import * as acorn from 'acorn';
import { walk } from 'zimmerframe';
import { tsPlugin } from '@sveltejs/acorn-typescript';
const ParserWithTS = acorn.Parser.extend(tsPlugin());
+/**
+ * @typedef {Comment & {
+ * start: number;
+ * end: number;
+ * }} CommentWithLocation
+ */
+
/**
* @param {string} source
+ * @param {AST.JSComment[]} comments
* @param {boolean} typescript
* @param {boolean} [is_script]
*/
-export function parse(source, typescript, is_script) {
+export function parse(source, comments, typescript, is_script) {
const parser = typescript ? ParserWithTS : acorn.Parser;
- const { onComment, add_comments } = get_comment_handlers(source);
+
+ const { onComment, add_comments } = get_comment_handlers(
+ source,
+ /** @type {CommentWithLocation[]} */ (comments)
+ );
+
// @ts-ignore
const parse_statement = parser.prototype.parseStatement;
@@ -36,7 +50,7 @@ export function parse(source, typescript, is_script) {
ast = parser.parse(source, {
onComment,
sourceType: 'module',
- ecmaVersion: 13,
+ ecmaVersion: 16,
locations: true
});
} finally {
@@ -53,18 +67,24 @@ export function parse(source, typescript, is_script) {
/**
* @param {string} source
+ * @param {Comment[]} comments
* @param {boolean} typescript
* @param {number} index
* @returns {acorn.Expression & { leadingComments?: CommentWithLocation[]; trailingComments?: CommentWithLocation[]; }}
*/
-export function parse_expression_at(source, typescript, index) {
+export function parse_expression_at(source, comments, typescript, index) {
const parser = typescript ? ParserWithTS : acorn.Parser;
- const { onComment, add_comments } = get_comment_handlers(source);
+
+ const { onComment, add_comments } = get_comment_handlers(
+ source,
+ /** @type {CommentWithLocation[]} */ (comments),
+ index
+ );
const ast = parser.parseExpressionAt(source, index, {
onComment,
sourceType: 'module',
- ecmaVersion: 13,
+ ecmaVersion: 16,
locations: true
});
@@ -78,26 +98,20 @@ export function parse_expression_at(source, typescript, index) {
* to add them after the fact. They are needed in order to support `svelte-ignore` comments
* in JS code and so that `prettier-plugin-svelte` doesn't remove all comments when formatting.
* @param {string} source
+ * @param {CommentWithLocation[]} comments
+ * @param {number} index
*/
-function get_comment_handlers(source) {
- /**
- * @typedef {Comment & {
- * start: number;
- * end: number;
- * }} CommentWithLocation
- */
-
- /** @type {CommentWithLocation[]} */
- const comments = [];
-
+function get_comment_handlers(source, comments, index = 0) {
return {
/**
* @param {boolean} block
* @param {string} value
* @param {number} start
* @param {number} end
+ * @param {import('acorn').Position} [start_loc]
+ * @param {import('acorn').Position} [end_loc]
*/
- onComment: (block, value, start, end) => {
+ onComment: (block, value, start, end, start_loc, end_loc) => {
if (block && /\n/.test(value)) {
let a = start;
while (a > 0 && source[a - 1] !== '\n') a -= 1;
@@ -109,13 +123,26 @@ function get_comment_handlers(source) {
value = value.replace(new RegExp(`^${indentation}`, 'gm'), '');
}
- comments.push({ type: block ? 'Block' : 'Line', value, start, end });
+ comments.push({
+ type: block ? 'Block' : 'Line',
+ value,
+ start,
+ end,
+ loc: {
+ start: /** @type {import('acorn').Position} */ (start_loc),
+ end: /** @type {import('acorn').Position} */ (end_loc)
+ }
+ });
},
/** @param {acorn.Node & { leadingComments?: CommentWithLocation[]; trailingComments?: CommentWithLocation[]; }} ast */
add_comments(ast) {
if (comments.length === 0) return;
+ comments = comments
+ .filter((comment) => comment.start >= index)
+ .map(({ type, value, start, end }) => ({ type, value, start, end }));
+
walk(ast, null, {
_(node, { next, path }) {
let comment;
diff --git a/packages/svelte/src/compiler/phases/1-parse/index.js b/packages/svelte/src/compiler/phases/1-parse/index.js
index 6cc5b58aa666..77cc2bf3fa43 100644
--- a/packages/svelte/src/compiler/phases/1-parse/index.js
+++ b/packages/svelte/src/compiler/phases/1-parse/index.js
@@ -1,4 +1,5 @@
/** @import { AST } from '#compiler' */
+/** @import { Comment } from 'estree' */
// @ts-expect-error acorn type definitions are borked in the release we use
import { isIdentifierStart, isIdentifierChar } from 'acorn';
import fragment from './state/fragment.js';
@@ -8,6 +9,7 @@ import { create_fragment } from './utils/create.js';
import read_options from './read/options.js';
import { is_reserved } from '../../../utils.js';
import { disallow_children } from '../2-analyze/visitors/shared/special-element.js';
+import * as state from '../../state.js';
const regex_position_indicator = / \(\d+:\d+\)$/;
@@ -87,6 +89,7 @@ export class Parser {
type: 'Root',
fragment: create_fragment(),
options: null,
+ comments: [],
metadata: {
ts: this.ts
}
@@ -299,6 +302,8 @@ export class Parser {
* @returns {AST.Root}
*/
export function parse(template, loose = false) {
+ state.set_source(template);
+
const parser = new Parser(template, loose);
return parser.root;
}
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 f4c73dcf403a..282288e2a22f 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);
}
@@ -57,7 +59,12 @@ export default function read_pattern(parser) {
space_with_newline.slice(0, first_space) + space_with_newline.slice(first_space + 1);
const expression = /** @type {any} */ (
- parse_expression_at(`${space_with_newline}(${pattern_string} = 1)`, parser.ts, start - 1)
+ parse_expression_at(
+ `${space_with_newline}(${pattern_string} = 1)`,
+ parser.root.comments,
+ parser.ts,
+ start - 1
+ )
).left;
expression.typeAnnotation = read_type_annotation(parser);
@@ -71,75 +78,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}
@@ -163,13 +101,13 @@ function read_type_annotation(parser) {
// parameters as part of a sequence expression instead, and will then error on optional
// parameters (`?:`). Therefore replace that sequence with something that will not error.
parser.template.slice(parser.index).replace(/\?\s*:/g, ':');
- let expression = parse_expression_at(template, parser.ts, a);
+ let expression = parse_expression_at(template, parser.root.comments, parser.ts, a);
// `foo: bar = baz` gets mangled — fix it
if (expression.type === 'AssignmentExpression') {
let b = expression.right.start;
while (template[b] !== '=') b -= 1;
- expression = parse_expression_at(template.slice(0, b), parser.ts, a);
+ expression = parse_expression_at(template.slice(0, b), parser.root.comments, parser.ts, a);
}
// `array as item: string, index` becomes `string, index`, which is mistaken as a sequence expression - fix that
diff --git a/packages/svelte/src/compiler/phases/1-parse/read/expression.js b/packages/svelte/src/compiler/phases/1-parse/read/expression.js
index a596cdf572cb..5d21f85792b0 100644
--- a/packages/svelte/src/compiler/phases/1-parse/read/expression.js
+++ b/packages/svelte/src/compiler/phases/1-parse/read/expression.js
@@ -34,12 +34,24 @@ export function get_loose_identifier(parser, opening_token) {
*/
export default function read_expression(parser, opening_token, disallow_loose) {
try {
- const node = parse_expression_at(parser.template, parser.ts, parser.index);
+ let comment_index = parser.root.comments.length;
+
+ const node = parse_expression_at(
+ parser.template,
+ parser.root.comments,
+ parser.ts,
+ parser.index
+ );
let num_parens = 0;
- if (node.leadingComments !== undefined && node.leadingComments.length > 0) {
- parser.index = node.leadingComments.at(-1).end;
+ let i = parser.root.comments.length;
+ while (i-- > comment_index) {
+ const comment = parser.root.comments[i];
+ if (comment.end < node.start) {
+ parser.index = comment.end;
+ break;
+ }
}
for (let i = parser.index; i < /** @type {number} */ (node.start); i += 1) {
@@ -47,9 +59,9 @@ export default function read_expression(parser, opening_token, disallow_loose) {
}
let index = /** @type {number} */ (node.end);
- if (node.trailingComments !== undefined && node.trailingComments.length > 0) {
- index = node.trailingComments.at(-1).end;
- }
+
+ const last_comment = parser.root.comments.at(-1);
+ if (last_comment && last_comment.end > index) index = last_comment.end;
while (num_parens > 0) {
const char = parser.template[index];
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 9d9ed3a1efdf..9ce449f20074 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) {
@@ -34,7 +34,7 @@ export function read_script(parser, start, attributes) {
let ast;
try {
- ast = acorn.parse(source, parser.ts, true);
+ ast = acorn.parse(source, parser.root.comments, parser.ts, true);
} catch (err) {
parser.acorn_error(err);
}
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 f8c39f1b1dd1..80ab234d925f 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) {
@@ -118,6 +119,7 @@ function read_rule(parser) {
metadata: {
parent_rule: null,
has_local_selectors: false,
+ has_global_selectors: false,
is_global_block: false
}
};
@@ -342,6 +344,7 @@ function read_selector(parser, inside_pseudo_class = false) {
children,
metadata: {
rule: null,
+ is_global: false,
used: false
}
};
@@ -578,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 09eb0bfa68c1..cb498c3c131a 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
@@ -1,7 +1,7 @@
/** @import { Context, Visitors } from 'zimmerframe' */
/** @import { FunctionExpression, FunctionDeclaration } from 'estree' */
import { walk } from 'zimmerframe';
-import * as b from '../../utils/builders.js';
+import * as b from '#compiler/builders';
import * as e from '../../errors.js';
/**
@@ -24,6 +24,7 @@ const visitors = {
// until that day comes, we just delete them so they don't confuse esrap
delete n.typeAnnotation;
delete n.typeParameters;
+ delete n.typeArguments;
delete n.returnType;
delete n.accessibility;
},
@@ -94,6 +95,9 @@ const visitors = {
TSTypeAliasDeclaration() {
return b.empty;
},
+ TSTypeAssertion(node, context) {
+ return context.visit(node.expression);
+ },
TSEnumDeclaration(node) {
e.typescript_invalid_feature(node, 'enums');
},
@@ -111,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 66946a8f8d22..6b6c9160d8d7 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 },
+ `${name}>`,
+ `${parent.name}>`
+ );
+ }
+ } else if (!parser.loose) {
if (parser.last_auto_closed_tag && parser.last_auto_closed_tag.tag === name) {
e.element_invalid_closing_tag_autoclosed(start, name, parser.last_auto_closed_tag.reason);
} else {
@@ -186,6 +195,8 @@ export default function element(parser) {
parser.allow_whitespace();
if (parent.type === 'RegularElement' && closing_tag_omitted(parent.name, name)) {
+ const end = parent.fragment.nodes[0]?.start ?? start;
+ w.element_implicitly_closed({ start: parent.start, end }, `<${name}>`, `${parent.name}>`);
parent.end = start;
parser.pop();
parser.last_auto_closed_tag = {
@@ -480,7 +491,7 @@ function read_static_attribute(parser) {
/**
* @param {Parser} parser
- * @returns {AST.Attribute | AST.SpreadAttribute | AST.Directive | null}
+ * @returns {AST.Attribute | AST.SpreadAttribute | AST.Directive | AST.AttachTag | null}
*/
function read_attribute(parser) {
const start = parser.index;
@@ -488,6 +499,27 @@ function read_attribute(parser) {
if (parser.eat('{')) {
parser.allow_whitespace();
+ if (parser.eat('@attach')) {
+ parser.require_whitespace();
+
+ const expression = read_expression(parser);
+ parser.allow_whitespace();
+ parser.eat('}', true);
+
+ /** @type {AST.AttachTag} */
+ const attachment = {
+ type: 'AttachTag',
+ start,
+ end: parser.index,
+ expression,
+ metadata: {
+ expression: create_expression_metadata()
+ }
+ };
+
+ return attachment;
+ }
+
if (parser.eat('...')) {
const expression = read_expression(parser);
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 0eb98c27e858..ba091ef7ec41 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;
@@ -60,7 +63,10 @@ function open(parser) {
end: -1,
test: read_expression(parser),
consequent: create_fragment(),
- alternate: null
+ alternate: null,
+ metadata: {
+ expression: create_expression_metadata()
+ }
});
parser.allow_whitespace();
@@ -241,7 +247,10 @@ function open(parser) {
error: null,
pending: null,
then: null,
- catch: null
+ catch: null,
+ metadata: {
+ expression: create_expression_metadata()
+ }
});
if (parser.eat('then')) {
@@ -323,7 +332,10 @@ function open(parser) {
start,
end: -1,
expression,
- fragment: create_fragment()
+ fragment: create_fragment(),
+ metadata: {
+ expression: create_expression_metadata()
+ }
});
parser.stack.push(block);
@@ -351,6 +363,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) {
@@ -370,7 +398,12 @@ function open(parser) {
let function_expression = matched
? /** @type {ArrowFunctionExpression} */ (
- parse_expression_at(prelude + `${params} => {}`, parser.ts, params_start)
+ parse_expression_at(
+ prelude + `${params} => {}`,
+ parser.root.comments,
+ parser.ts,
+ params_start
+ )
)
: { params: [] };
@@ -388,6 +421,7 @@ function open(parser) {
end: name_end,
name
},
+ typeParams: type_params,
parameters: function_expression.params,
body: create_fragment(),
metadata: {
@@ -441,7 +475,10 @@ function next(parser) {
elseif: true,
test: expression,
consequent: create_fragment(),
- alternate: null
+ alternate: null,
+ metadata: {
+ expression: create_expression_metadata()
+ }
});
parser.stack.push(child);
@@ -604,7 +641,10 @@ function special(parser) {
type: 'HtmlTag',
start,
end: parser.index,
- expression
+ expression,
+ metadata: {
+ expression: create_expression_metadata()
+ }
});
return;
@@ -679,6 +719,9 @@ function special(parser) {
declarations: [{ type: 'VariableDeclarator', id, init, start: id.start, end: init.end }],
start: start + 2, // start at const, not at @const
end: parser.index - 1
+ },
+ metadata: {
+ expression: create_expression_metadata()
}
});
}
@@ -705,6 +748,7 @@ function special(parser) {
end: parser.index,
expression: /** @type {AST.RenderTag['expression']} */ (expression),
metadata: {
+ expression: create_expression_metadata(),
dynamic: false,
arguments: [],
path: [],
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 b7c8cb43cd00..8c69a58c9980 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/1-parse/utils/fuzzymatch.js b/packages/svelte/src/compiler/phases/1-parse/utils/fuzzymatch.js
index cd72d73005c2..28b314cdd567 100644
--- a/packages/svelte/src/compiler/phases/1-parse/utils/fuzzymatch.js
+++ b/packages/svelte/src/compiler/phases/1-parse/utils/fuzzymatch.js
@@ -12,8 +12,8 @@ export default function fuzzymatch(name, names) {
return matches && matches[0][0] > 0.7 ? matches[0][1] : null;
}
-// adapted from https://github.com/Glench/fuzzyset.js/blob/master/lib/fuzzyset.js
-// BSD Licensed
+// adapted from https://github.com/Glench/fuzzyset.js/blob/master/lib/fuzzyset.js in 2016
+// BSD Licensed (see https://github.com/Glench/fuzzyset.js/issues/10)
const GRAM_SIZE_LOWER = 2;
const GRAM_SIZE_UPPER = 3;
diff --git a/packages/svelte/src/compiler/phases/1-parse/utils/html.js b/packages/svelte/src/compiler/phases/1-parse/utils/html.js
index a68acb996faf..a0c2a5b06ffd 100644
--- a/packages/svelte/src/compiler/phases/1-parse/utils/html.js
+++ b/packages/svelte/src/compiler/phases/1-parse/utils/html.js
@@ -72,6 +72,8 @@ const NUL = 0;
// to replace them ourselves
//
// Source: http://en.wikipedia.org/wiki/Character_encodings_in_HTML#Illegal_characters
+// Also see: https://en.wikipedia.org/wiki/Plane_(Unicode)
+// Also see: https://html.spec.whatwg.org/multipage/parsing.html#preprocessing-the-input-stream
/** @param {number} code */
function validate_code(code) {
@@ -116,5 +118,10 @@ function validate_code(code) {
return code;
}
+ // supplementary special-purpose plane 0xe0000 - 0xe07f and 0xe0100 - 0xe01ef
+ if ((code >= 917504 && code <= 917631) || (code >= 917760 && code <= 917999)) {
+ return code;
+ }
+
return NUL;
}
diff --git a/packages/svelte/src/compiler/phases/2-analyze/css/css-analyze.js b/packages/svelte/src/compiler/phases/2-analyze/css/css-analyze.js
index ed228385820a..d6052c9c3e4d 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/css/css-analyze.js
+++ b/packages/svelte/src/compiler/phases/2-analyze/css/css-analyze.js
@@ -7,13 +7,15 @@ import { is_keyframes_node } from '../../css.js';
import { is_global, is_unscoped_pseudo_class } from './utils.js';
/**
- * @typedef {Visitors<
- * AST.CSS.Node,
- * {
- * keyframes: string[];
- * rule: AST.CSS.Rule | null;
- * }
- * >} CssVisitors
+ * @typedef {{
+ * keyframes: string[];
+ * rule: AST.CSS.Rule | null;
+ * analysis: ComponentAnalysis;
+ * }} CssState
+ */
+
+/**
+ * @typedef {Visitors} CssVisitors
*/
/**
@@ -28,6 +30,15 @@ function is_global_block_selector(simple_selector) {
);
}
+/**
+ * @param {AST.SvelteNode[]} path
+ */
+function is_unscoped(path) {
+ return path
+ .filter((node) => node.type === 'Rule')
+ .every((node) => node.metadata.has_global_selectors);
+}
+
/**
*
* @param {Array} path
@@ -42,6 +53,9 @@ const css_visitors = {
if (is_keyframes_node(node)) {
if (!node.prelude.startsWith('-global-') && !is_in_global_block(context.path)) {
context.state.keyframes.push(node.prelude);
+ } else if (node.prelude.startsWith('-global-')) {
+ // we don't check if the block.children.length because the keyframe is still added even if empty
+ context.state.analysis.css.has_global ||= is_unscoped(context.path);
}
}
@@ -54,8 +68,12 @@ const css_visitors = {
const global = node.children.find(is_global);
if (global) {
- const idx = node.children.indexOf(global);
+ const is_nested = context.path.at(-2)?.type === 'PseudoClassSelector';
+ if (is_nested && !global.selectors[0].args) {
+ e.css_global_block_invalid_placement(global.selectors[0]);
+ }
+ const idx = node.children.indexOf(global);
if (global.selectors[0].args !== null && idx !== 0 && idx !== node.children.length - 1) {
// ensure `:global(...)` is not used in the middle of a selector (but multiple `global(...)` in sequence are ok)
for (let i = idx + 1; i < node.children.length; i++) {
@@ -99,10 +117,12 @@ const css_visitors = {
node.metadata.rule = context.state.rule;
- node.metadata.used ||= node.children.every(
+ node.metadata.is_global = node.children.every(
({ metadata }) => metadata.is_global || metadata.is_global_like
);
+ node.metadata.used ||= node.metadata.is_global;
+
if (
node.metadata.rule?.metadata.parent_rule &&
node.children[0]?.selectors[0]?.type === 'NestingSelector'
@@ -133,7 +153,13 @@ const css_visitors = {
node.metadata.is_global = node.selectors.length >= 1 && is_global(node);
- if (node.selectors.length === 1) {
+ if (
+ node.selectors.length >= 1 &&
+ node.selectors.every(
+ (selector) =>
+ selector.type === 'PseudoClassSelector' || selector.type === 'PseudoElementSelector'
+ )
+ ) {
const first = node.selectors[0];
node.metadata.is_global_like ||=
(first.type === 'PseudoClassSelector' && first.name === 'host') ||
@@ -171,10 +197,12 @@ const css_visitors = {
Rule(node, context) {
node.metadata.parent_rule = context.state.rule;
- node.metadata.is_global_block = node.prelude.children.some((selector) => {
+ // We gotta allow :global x, :global y because CSS preprocessors might generate that from :global { x, y {...} }
+ for (const complex_selector of node.prelude.children) {
let is_global_block = false;
- for (const child of selector.children) {
+ for (let selector_idx = 0; selector_idx < complex_selector.children.length; selector_idx++) {
+ const child = complex_selector.children[selector_idx];
const idx = child.selectors.findIndex(is_global_block_selector);
if (is_global_block) {
@@ -182,70 +210,79 @@ const css_visitors = {
child.metadata.is_global_like = true;
}
- if (idx !== -1) {
- is_global_block = true;
- for (let i = idx + 1; i < child.selectors.length; i++) {
- walk(/** @type {AST.CSS.Node} */ (child.selectors[i]), null, {
- ComplexSelector(node) {
- node.metadata.used = true;
- }
- });
- }
- }
- }
+ if (idx === 0) {
+ if (
+ child.selectors.length > 1 &&
+ selector_idx === 0 &&
+ node.metadata.parent_rule === null
+ ) {
+ e.css_global_block_invalid_modifier_start(child.selectors[1]);
+ } else {
+ // `child` starts with `:global`
+ node.metadata.is_global_block = is_global_block = true;
+
+ for (let i = 1; i < child.selectors.length; i++) {
+ walk(/** @type {AST.CSS.Node} */ (child.selectors[i]), null, {
+ ComplexSelector(node) {
+ node.metadata.used = true;
+ }
+ });
+ }
- return is_global_block;
- });
+ if (child.combinator && child.combinator.name !== ' ') {
+ e.css_global_block_invalid_combinator(child, child.combinator.name);
+ }
- if (node.metadata.is_global_block) {
- if (node.prelude.children.length > 1) {
- e.css_global_block_invalid_list(node.prelude);
- }
+ const declaration = node.block.children.find((child) => child.type === 'Declaration');
+ const is_lone_global =
+ complex_selector.children.length === 1 &&
+ complex_selector.children[0].selectors.length === 1; // just `:global`, not e.g. `:global x`
- const complex_selector = node.prelude.children[0];
- const global_selector = complex_selector.children.find((r, selector_idx) => {
- const idx = r.selectors.findIndex(is_global_block_selector);
- if (idx === 0) {
- if (r.selectors.length > 1 && selector_idx === 0 && node.metadata.parent_rule === null) {
- e.css_global_block_invalid_modifier_start(r.selectors[1]);
+ if (is_lone_global && node.prelude.children.length > 1) {
+ // `:global, :global x { z { ... } }` would become `x { z { ... } }` which means `z` is always
+ // constrained by `x`, which is not what the user intended
+ e.css_global_block_invalid_list(node.prelude);
+ }
+
+ if (
+ declaration &&
+ // :global { color: red; } is invalid, but foo :global { color: red; } is valid
+ node.prelude.children.length === 1 &&
+ is_lone_global
+ ) {
+ e.css_global_block_invalid_declaration(declaration);
+ }
}
- return true;
} else if (idx !== -1) {
- e.css_global_block_invalid_modifier(r.selectors[idx]);
+ e.css_global_block_invalid_modifier(child.selectors[idx]);
}
- });
-
- if (!global_selector) {
- throw new Error('Internal error: global block without :global selector');
}
- if (global_selector.combinator && global_selector.combinator.name !== ' ') {
- e.css_global_block_invalid_combinator(global_selector, global_selector.combinator.name);
+ if (node.metadata.is_global_block && !is_global_block) {
+ e.css_global_block_invalid_list(node.prelude);
}
+ }
- const declaration = node.block.children.find((child) => child.type === 'Declaration');
+ const state = { ...context.state, rule: node };
- if (
- declaration &&
- // :global { color: red; } is invalid, but foo :global { color: red; } is valid
- node.prelude.children.length === 1 &&
- node.prelude.children[0].children.length === 1 &&
- node.prelude.children[0].children[0].selectors.length === 1
- ) {
- e.css_global_block_invalid_declaration(declaration);
- }
+ // visit selector list first, to populate child selector metadata
+ context.visit(node.prelude, state);
+
+ for (const selector of node.prelude.children) {
+ node.metadata.has_global_selectors ||= selector.metadata.is_global;
+ node.metadata.has_local_selectors ||= !selector.metadata.is_global;
}
- context.next({
- ...context.state,
- rule: node
- });
+ // if this rule has a ComplexSelector whose RelativeSelector children are all
+ // `:global(...)`, and the rule contains declarations (rather than just
+ // nested rules) then the component as a whole includes global CSS
+ context.state.analysis.css.has_global ||=
+ node.metadata.has_global_selectors &&
+ node.block.children.filter((child) => child.type === 'Declaration').length > 0 &&
+ is_unscoped(context.path);
- node.metadata.has_local_selectors = node.prelude.children.some((selector) => {
- return selector.children.some(
- ({ metadata }) => !metadata.is_global && !metadata.is_global_like
- );
- });
+ // visit block list, so parent rule metadata is populated
+ context.visit(node.block, state);
},
NestingSelector(node, context) {
const rule = /** @type {AST.CSS.Rule} */ (context.state.rule);
@@ -283,5 +320,12 @@ const css_visitors = {
* @param {ComponentAnalysis} analysis
*/
export function analyze_css(stylesheet, analysis) {
- walk(stylesheet, { keyframes: analysis.css.keyframes, rule: null }, css_visitors);
+ /** @type {CssState} */
+ const css_state = {
+ keyframes: analysis.css.keyframes,
+ rule: null,
+ analysis
+ };
+
+ walk(stylesheet, css_state, css_visitors);
}
diff --git a/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js b/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js
index fc8108e46e8e..79e8fbb02c08 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js
+++ b/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js
@@ -1,13 +1,21 @@
/** @import * as Compiler from '#compiler' */
import { walk } from 'zimmerframe';
-import { get_parent_rules, get_possible_values, is_outer_global } from './utils.js';
+import {
+ get_parent_rules,
+ get_possible_values,
+ is_outer_global,
+ is_unscoped_pseudo_class
+} from './utils.js';
import { regex_ends_with_whitespace, regex_starts_with_whitespace } from '../../patterns.js';
import { get_attribute_chunks, is_text_attribute } from '../../../utils/ast.js';
/** @typedef {NODE_PROBABLY_EXISTS | NODE_DEFINITELY_EXISTS} NodeExistsValue */
+/** @typedef {FORWARD | BACKWARD} Direction */
const NODE_PROBABLY_EXISTS = 0;
const NODE_DEFINITELY_EXISTS = 1;
+const FORWARD = 0;
+const BACKWARD = 1;
const whitelist_attribute_selector = new Map([
['details', ['open']],
@@ -43,6 +51,27 @@ const nesting_selector = {
}
};
+/** @type {Compiler.AST.CSS.RelativeSelector} */
+const any_selector = {
+ type: 'RelativeSelector',
+ start: -1,
+ end: -1,
+ combinator: null,
+ selectors: [
+ {
+ type: 'TypeSelector',
+ name: '*',
+ start: -1,
+ end: -1
+ }
+ ],
+ metadata: {
+ is_global: false,
+ is_global_like: false,
+ scoped: false
+ }
+};
+
/**
* Snippets encountered already (avoids infinite loops)
* @type {Set}
@@ -72,7 +101,8 @@ export function prune(stylesheet, element) {
apply_selector(
selectors,
/** @type {Compiler.AST.CSS.Rule} */ (node.metadata.rule),
- element
+ element,
+ BACKWARD
)
) {
node.metadata.used = true;
@@ -159,16 +189,17 @@ function truncate(node) {
* @param {Compiler.AST.CSS.RelativeSelector[]} relative_selectors
* @param {Compiler.AST.CSS.Rule} rule
* @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement} element
+ * @param {Direction} direction
* @returns {boolean}
*/
-function apply_selector(relative_selectors, rule, element) {
- const parent_selectors = relative_selectors.slice();
- const relative_selector = parent_selectors.pop();
+function apply_selector(relative_selectors, rule, element, direction) {
+ const rest_selectors = relative_selectors.slice();
+ const relative_selector = direction === FORWARD ? rest_selectors.shift() : rest_selectors.pop();
const matched =
!!relative_selector &&
- relative_selector_might_apply_to_node(relative_selector, rule, element) &&
- apply_combinator(relative_selector, parent_selectors, rule, element);
+ relative_selector_might_apply_to_node(relative_selector, rule, element, direction) &&
+ apply_combinator(relative_selector, rest_selectors, rule, element, direction);
if (matched) {
if (!is_outer_global(relative_selector)) {
@@ -183,76 +214,67 @@ function apply_selector(relative_selectors, rule, element) {
/**
* @param {Compiler.AST.CSS.RelativeSelector} relative_selector
- * @param {Compiler.AST.CSS.RelativeSelector[]} parent_selectors
+ * @param {Compiler.AST.CSS.RelativeSelector[]} rest_selectors
* @param {Compiler.AST.CSS.Rule} rule
* @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement | Compiler.AST.RenderTag | Compiler.AST.Component | Compiler.AST.SvelteComponent | Compiler.AST.SvelteSelf} node
+ * @param {Direction} direction
* @returns {boolean}
*/
-function apply_combinator(relative_selector, parent_selectors, rule, node) {
- if (!relative_selector.combinator) return true;
-
- const name = relative_selector.combinator.name;
+function apply_combinator(relative_selector, rest_selectors, rule, node, direction) {
+ const combinator =
+ direction == FORWARD ? rest_selectors[0]?.combinator : relative_selector.combinator;
+ if (!combinator) return true;
- switch (name) {
+ switch (combinator.name) {
case ' ':
case '>': {
+ const is_adjacent = combinator.name === '>';
+ const parents =
+ direction === FORWARD
+ ? get_descendant_elements(node, is_adjacent)
+ : get_ancestor_elements(node, is_adjacent);
let parent_matched = false;
- const path = node.metadata.path;
- let i = path.length;
-
- while (i--) {
- const parent = path[i];
-
- if (parent.type === 'SnippetBlock') {
- if (seen.has(parent)) {
- parent_matched = true;
- } else {
- seen.add(parent);
-
- for (const site of parent.metadata.sites) {
- if (apply_combinator(relative_selector, parent_selectors, rule, site)) {
- parent_matched = true;
- }
- }
- }
-
- break;
- }
-
- if (parent.type === 'RegularElement' || parent.type === 'SvelteElement') {
- if (apply_selector(parent_selectors, rule, parent)) {
- parent_matched = true;
- }
-
- if (name === '>') return parent_matched;
+ for (const parent of parents) {
+ if (apply_selector(rest_selectors, rule, parent, direction)) {
+ parent_matched = true;
}
}
- return parent_matched || parent_selectors.every((selector) => is_global(selector, rule));
+ return (
+ parent_matched ||
+ (direction === BACKWARD &&
+ (!is_adjacent || parents.length === 0) &&
+ rest_selectors.every((selector) => is_global(selector, rule)))
+ );
}
case '+':
case '~': {
- const siblings = get_possible_element_siblings(node, name === '+');
+ const siblings = get_possible_element_siblings(node, direction, combinator.name === '+');
let sibling_matched = false;
for (const possible_sibling of siblings.keys()) {
- if (possible_sibling.type === 'RenderTag' || possible_sibling.type === 'SlotElement') {
+ if (
+ possible_sibling.type === 'RenderTag' ||
+ possible_sibling.type === 'SlotElement' ||
+ possible_sibling.type === 'Component'
+ ) {
// `{@render foo()}foo
` with `:global(.x) + p` is a match
- if (parent_selectors.length === 1 && parent_selectors[0].metadata.is_global) {
+ if (rest_selectors.length === 1 && rest_selectors[0].metadata.is_global) {
sibling_matched = true;
}
- } else if (apply_selector(parent_selectors, rule, possible_sibling)) {
+ } else if (apply_selector(rest_selectors, rule, possible_sibling, direction)) {
sibling_matched = true;
}
}
return (
sibling_matched ||
- (get_element_parent(node) === null &&
- parent_selectors.every((selector) => is_global(selector, rule)))
+ (direction === BACKWARD &&
+ get_element_parent(node) === null &&
+ rest_selectors.every((selector) => is_global(selector, rule)))
);
}
@@ -269,20 +291,26 @@ function apply_combinator(relative_selector, parent_selectors, rule, node) {
* a global selector
* @param {Compiler.AST.CSS.RelativeSelector} selector
* @param {Compiler.AST.CSS.Rule} rule
+ * @returns {boolean}
*/
function is_global(selector, rule) {
if (selector.metadata.is_global || selector.metadata.is_global_like) {
return true;
}
+ let explicitly_global = false;
+
for (const s of selector.selectors) {
/** @type {Compiler.AST.CSS.SelectorList | null} */
let selector_list = null;
+ let can_be_global = false;
let owner = rule;
if (s.type === 'PseudoClassSelector') {
if ((s.name === 'is' || s.name === 'where') && s.args) {
selector_list = s.args;
+ } else {
+ can_be_global = is_unscoped_pseudo_class(s);
}
}
@@ -291,18 +319,19 @@ function is_global(selector, rule) {
selector_list = owner.prelude;
}
- const has_global_selectors = selector_list?.children.some((complex_selector) => {
+ const has_global_selectors = !!selector_list?.children.some((complex_selector) => {
return complex_selector.children.every((relative_selector) =>
is_global(relative_selector, owner)
);
});
+ explicitly_global ||= has_global_selectors;
- if (!has_global_selectors) {
+ if (!has_global_selectors && !can_be_global) {
return false;
}
}
- return true;
+ return explicitly_global || selector.selectors.length === 0;
}
const regex_backslash_and_following_character = /\\(.)/g;
@@ -313,9 +342,10 @@ const regex_backslash_and_following_character = /\\(.)/g;
* @param {Compiler.AST.CSS.RelativeSelector} relative_selector
* @param {Compiler.AST.CSS.Rule} rule
* @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement} element
+ * @param {Direction} direction
* @returns {boolean}
*/
-function relative_selector_might_apply_to_node(relative_selector, rule, element) {
+function relative_selector_might_apply_to_node(relative_selector, rule, element, direction) {
// Sort :has(...) selectors in one bucket and everything else into another
const has_selectors = [];
const other_selectors = [];
@@ -331,13 +361,6 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element)
// If we're called recursively from a :has(...) selector, we're on the way of checking if the other selectors match.
// In that case ignore this check (because we just came from this) to avoid an infinite loop.
if (has_selectors.length > 0) {
- /** @type {Array} */
- const child_elements = [];
- /** @type {Array} */
- const descendant_elements = [];
- /** @type {Array} */
- let sibling_elements; // do them lazy because it's rarely used and expensive to calculate
-
// If this is a :has inside a global selector, we gotta include the element itself, too,
// because the global selector might be for an element that's outside the component,
// e.g. :root:has(.scoped), :global(.foo):has(.scoped), or :root { &:has(.scoped) {} }
@@ -353,46 +376,6 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element)
)
)
);
- if (include_self) {
- child_elements.push(element);
- descendant_elements.push(element);
- }
-
- const seen = new Set();
-
- /**
- * @param {Compiler.AST.SvelteNode} node
- * @param {{ is_child: boolean }} state
- */
- function walk_children(node, state) {
- walk(node, state, {
- _(node, context) {
- if (node.type === 'RegularElement' || node.type === 'SvelteElement') {
- descendant_elements.push(node);
-
- if (context.state.is_child) {
- child_elements.push(node);
- context.state.is_child = false;
- context.next();
- context.state.is_child = true;
- } else {
- context.next();
- }
- } else if (node.type === 'RenderTag') {
- for (const snippet of node.metadata.snippets) {
- if (seen.has(snippet)) continue;
-
- seen.add(snippet);
- walk_children(snippet.body, context.state);
- }
- } else {
- context.next();
- }
- }
- });
- }
-
- walk_children(element.fragment, { is_child: true });
// :has(...) is special in that it means "look downwards in the CSS tree". Since our matching algorithm goes
// upwards and back-to-front, we need to first check the selectors inside :has(...), then check the rest of the
@@ -403,37 +386,34 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element)
let matched = false;
for (const complex_selector of complex_selectors) {
- const selectors = truncate(complex_selector);
- const left_most_combinator = selectors[0]?.combinator ?? descendant_combinator;
- // In .x:has(> y), we want to search for y, ignoring the left-most combinator
- // (else it would try to walk further up and fail because there are no selectors left)
- if (selectors.length > 0) {
- selectors[0] = {
- ...selectors[0],
- combinator: null
- };
+ const [first, ...rest] = truncate(complex_selector);
+ // if it was just a :global(...)
+ if (!first) {
+ complex_selector.metadata.used = true;
+ matched = true;
+ continue;
}
- const descendants =
- left_most_combinator.name === '+' || left_most_combinator.name === '~'
- ? (sibling_elements ??= get_following_sibling_elements(element, include_self))
- : left_most_combinator.name === '>'
- ? child_elements
- : descendant_elements;
-
- let selector_matched = false;
-
- // Iterate over all descendant elements and check if the selector inside :has matches
- for (const element of descendants) {
- if (
- selectors.length === 0 /* is :global(...) */ ||
- (element.metadata.scoped && selector_matched) ||
- apply_selector(selectors, rule, element)
- ) {
+ if (include_self) {
+ const selector_including_self = [
+ first.combinator ? { ...first, combinator: null } : first,
+ ...rest
+ ];
+ if (apply_selector(selector_including_self, rule, element, FORWARD)) {
complex_selector.metadata.used = true;
- selector_matched = matched = true;
+ matched = true;
}
}
+
+ const selector_excluding_self = [
+ any_selector,
+ first.combinator ? first : { ...first, combinator: descendant_combinator },
+ ...rest
+ ];
+ if (apply_selector(selector_excluding_self, rule, element, FORWARD)) {
+ complex_selector.metadata.used = true;
+ matched = true;
+ }
}
if (!matched) {
@@ -458,7 +438,7 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element)
) {
const args = selector.args;
const complex_selector = args.children[0];
- return apply_selector(complex_selector.children, rule, element);
+ return apply_selector(complex_selector.children, rule, element, BACKWARD);
}
// We came across a :global, everything beyond it is global and therefore a potential match
@@ -507,7 +487,7 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element)
if (is_global) {
complex_selector.metadata.used = true;
matched = true;
- } else if (apply_selector(relative, rule, element)) {
+ } else if (apply_selector(relative, rule, element, BACKWARD)) {
complex_selector.metadata.used = true;
matched = true;
} else if (complex_selector.children.length > 1 && (name == 'is' || name == 'where')) {
@@ -552,12 +532,7 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element)
}
case 'ClassSelector': {
- if (
- !attribute_matches(element, 'class', name, '~=', false) &&
- !element.attributes.some(
- (attribute) => attribute.type === 'ClassDirective' && attribute.name === name
- )
- ) {
+ if (!attribute_matches(element, 'class', name, '~=', false)) {
return false;
}
@@ -591,7 +566,7 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element)
for (const complex_selector of parent.prelude.children) {
if (
- apply_selector(get_relative_selectors(complex_selector), parent, element) ||
+ apply_selector(get_relative_selectors(complex_selector), parent, element, direction) ||
complex_selector.children.every((s) => is_global(s, parent))
) {
complex_selector.metadata.used = true;
@@ -612,80 +587,6 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element)
return true;
}
-/**
- * @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement} element
- * @param {boolean} include_self
- */
-function get_following_sibling_elements(element, include_self) {
- const path = element.metadata.path;
- let i = path.length;
-
- /** @type {Compiler.AST.SvelteNode} */
- let start = element;
- let nodes = /** @type {Compiler.AST.SvelteNode[]} */ (
- /** @type {Compiler.AST.Fragment} */ (path[0]).nodes
- );
-
- // find the set of nodes to walk...
- while (i--) {
- const node = path[i];
-
- if (node.type === 'RegularElement' || node.type === 'SvelteElement') {
- nodes = node.fragment.nodes;
- break;
- }
-
- if (node.type !== 'Fragment') {
- start = node;
- }
- }
-
- /** @type {Array} */
- const siblings = [];
-
- // ...then walk them, starting from the node containing the element in question
- // skipping nodes that appears before the element
-
- const seen = new Set();
- let skip = true;
-
- /** @param {Compiler.AST.SvelteNode} node */
- function get_siblings(node) {
- walk(node, null, {
- RegularElement(node) {
- if (node === element) {
- skip = false;
- if (include_self) siblings.push(node);
- } else if (!skip) {
- siblings.push(node);
- }
- },
- SvelteElement(node) {
- if (node === element) {
- skip = false;
- if (include_self) siblings.push(node);
- } else if (!skip) {
- siblings.push(node);
- }
- },
- RenderTag(node) {
- for (const snippet of node.metadata.snippets) {
- if (seen.has(snippet)) continue;
-
- seen.add(snippet);
- get_siblings(snippet.body);
- }
- }
- });
- }
-
- for (const node of nodes.slice(nodes.indexOf(start))) {
- get_siblings(node);
- }
-
- return siblings;
-}
-
/**
* @param {any} operator
* @param {any} expected_value
@@ -727,14 +628,33 @@ function attribute_matches(node, name, expected_value, operator, case_insensitiv
if (attribute.type === 'SpreadAttribute') return true;
if (attribute.type === 'BindDirective' && attribute.name === name) return true;
+ const name_lower = name.toLowerCase();
+ // match attributes against the corresponding directive but bail out on exact matching
+ if (attribute.type === 'StyleDirective' && name_lower === 'style') return true;
+ if (attribute.type === 'ClassDirective' && name_lower === 'class') {
+ if (operator === '~=') {
+ if (attribute.name === expected_value) return true;
+ } else {
+ return true;
+ }
+ }
+
if (attribute.type !== 'Attribute') continue;
- if (attribute.name.toLowerCase() !== name.toLowerCase()) continue;
+ if (attribute.name.toLowerCase() !== name_lower) continue;
if (attribute.value === true) return operator === null;
if (expected_value === null) return true;
if (is_text_attribute(attribute)) {
- return test_attribute(operator, expected_value, case_insensitive, attribute.value[0].data);
+ const matches = test_attribute(
+ operator,
+ expected_value,
+ case_insensitive,
+ attribute.value[0].data
+ );
+ // continue if we still may match against a class/style directive
+ if (!matches && (name_lower === 'class' || name_lower === 'style')) continue;
+ return matches;
}
const chunks = get_attribute_chunks(attribute.value);
@@ -743,7 +663,7 @@ function attribute_matches(node, name, expected_value, operator, case_insensitiv
/** @type {string[]} */
let prev_values = [];
for (const chunk of chunks) {
- const current_possible_values = get_possible_values(chunk, name === 'class');
+ const current_possible_values = get_possible_values(chunk, name_lower === 'class');
// impossible to find out all combinations
if (!current_possible_values) return true;
@@ -822,6 +742,84 @@ function unquote(str) {
return str;
}
+/**
+ * @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement | Compiler.AST.RenderTag | Compiler.AST.Component | Compiler.AST.SvelteComponent | Compiler.AST.SvelteSelf} node
+ * @param {boolean} adjacent_only
+ * @param {Set} seen
+ */
+function get_ancestor_elements(node, adjacent_only, seen = new Set()) {
+ /** @type {Array} */
+ const ancestors = [];
+
+ const path = node.metadata.path;
+ let i = path.length;
+
+ while (i--) {
+ const parent = path[i];
+
+ if (parent.type === 'SnippetBlock') {
+ if (!seen.has(parent)) {
+ seen.add(parent);
+
+ for (const site of parent.metadata.sites) {
+ ancestors.push(...get_ancestor_elements(site, adjacent_only, seen));
+ }
+ }
+
+ break;
+ }
+
+ if (parent.type === 'RegularElement' || parent.type === 'SvelteElement') {
+ ancestors.push(parent);
+ if (adjacent_only) {
+ break;
+ }
+ }
+ }
+
+ return ancestors;
+}
+
+/**
+ * @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement | Compiler.AST.RenderTag | Compiler.AST.Component | Compiler.AST.SvelteComponent | Compiler.AST.SvelteSelf} node
+ * @param {boolean} adjacent_only
+ * @param {Set} seen
+ */
+function get_descendant_elements(node, adjacent_only, seen = new Set()) {
+ /** @type {Array} */
+ const descendants = [];
+
+ /**
+ * @param {Compiler.AST.SvelteNode} node
+ */
+ function walk_children(node) {
+ walk(node, null, {
+ _(node, context) {
+ if (node.type === 'RegularElement' || node.type === 'SvelteElement') {
+ descendants.push(node);
+
+ if (!adjacent_only) {
+ context.next();
+ }
+ } else if (node.type === 'RenderTag') {
+ for (const snippet of node.metadata.snippets) {
+ if (seen.has(snippet)) continue;
+
+ seen.add(snippet);
+ walk_children(snippet.body);
+ }
+ } else {
+ context.next();
+ }
+ }
+ });
+ }
+
+ walk_children(node.type === 'RenderTag' ? node : node.fragment);
+
+ return descendants;
+}
+
/**
* @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement | Compiler.AST.RenderTag | Compiler.AST.Component | Compiler.AST.SvelteComponent | Compiler.AST.SvelteSelf} node
* @returns {Compiler.AST.RegularElement | Compiler.AST.SvelteElement | null}
@@ -843,12 +841,13 @@ function get_element_parent(node) {
/**
* @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement | Compiler.AST.RenderTag | Compiler.AST.Component | Compiler.AST.SvelteComponent | Compiler.AST.SvelteSelf} node
+ * @param {Direction} direction
* @param {boolean} adjacent_only
* @param {Set} seen
- * @returns {Map}
+ * @returns {Map}
*/
-function get_possible_element_siblings(node, adjacent_only, seen = new Set()) {
- /** @type {Map} */
+function get_possible_element_siblings(node, direction, adjacent_only, seen = new Set()) {
+ /** @type {Map} */
const result = new Map();
const path = node.metadata.path;
@@ -859,9 +858,9 @@ function get_possible_element_siblings(node, adjacent_only, seen = new Set()) {
while (i--) {
const fragment = /** @type {Compiler.AST.Fragment} */ (path[i--]);
- let j = fragment.nodes.indexOf(current);
+ let j = fragment.nodes.indexOf(current) + (direction === FORWARD ? 1 : -1);
- while (j--) {
+ while (j >= 0 && j < fragment.nodes.length) {
const node = fragment.nodes[j];
if (node.type === 'RegularElement') {
@@ -876,21 +875,32 @@ function get_possible_element_siblings(node, adjacent_only, seen = new Set()) {
return result;
}
}
- } else if (is_block(node)) {
- if (node.type === 'SlotElement') {
+ // Special case: slots, render tags and svelte:element tags could resolve to no siblings,
+ // so we want to continue until we find a definite sibling even with the adjacent-only combinator
+ } else if (is_block(node) || node.type === 'Component') {
+ if (node.type === 'SlotElement' || node.type === 'Component') {
result.set(node, NODE_PROBABLY_EXISTS);
}
- const possible_last_child = get_possible_last_child(node, adjacent_only);
+ const possible_last_child = get_possible_nested_siblings(node, direction, adjacent_only);
add_to_map(possible_last_child, result);
- if (adjacent_only && has_definite_elements(possible_last_child)) {
+ if (
+ adjacent_only &&
+ node.type !== 'Component' &&
+ has_definite_elements(possible_last_child)
+ ) {
return result;
}
- } else if (node.type === 'RenderTag' || node.type === 'SvelteElement') {
+ } else if (node.type === 'SvelteElement') {
result.set(node, NODE_PROBABLY_EXISTS);
- // Special case: slots, render tags and svelte:element tags could resolve to no siblings,
- // so we want to continue until we find a definite sibling even with the adjacent-only combinator
+ } else if (node.type === 'RenderTag') {
+ result.set(node, NODE_PROBABLY_EXISTS);
+ for (const snippet of node.metadata.snippets) {
+ add_to_map(get_possible_nested_siblings(snippet, direction, adjacent_only), result);
+ }
}
+
+ j = direction === FORWARD ? j + 1 : j - 1;
}
current = path[i];
@@ -910,7 +920,7 @@ function get_possible_element_siblings(node, adjacent_only, seen = new Set()) {
seen.add(current);
for (const site of current.metadata.sites) {
- const siblings = get_possible_element_siblings(site, adjacent_only, seen);
+ const siblings = get_possible_element_siblings(site, direction, adjacent_only, seen);
add_to_map(siblings, result);
if (adjacent_only && current.metadata.sites.size === 1 && has_definite_elements(siblings)) {
@@ -923,7 +933,7 @@ function get_possible_element_siblings(node, adjacent_only, seen = new Set()) {
if (current.type === 'EachBlock' && fragment === current.body) {
// `{#each ...} {/each}` — `` can be previous sibling of ` `
- add_to_map(get_possible_last_child(current, adjacent_only), result);
+ add_to_map(get_possible_nested_siblings(current, direction, adjacent_only), result);
}
}
@@ -931,11 +941,13 @@ function get_possible_element_siblings(node, adjacent_only, seen = new Set()) {
}
/**
- * @param {Compiler.AST.EachBlock | Compiler.AST.IfBlock | Compiler.AST.AwaitBlock | Compiler.AST.KeyBlock | Compiler.AST.SlotElement} node
+ * @param {Compiler.AST.EachBlock | Compiler.AST.IfBlock | Compiler.AST.AwaitBlock | Compiler.AST.KeyBlock | Compiler.AST.SlotElement | Compiler.AST.SnippetBlock | Compiler.AST.Component} node
+ * @param {Direction} direction
* @param {boolean} adjacent_only
+ * @param {Set} seen
* @returns {Map}
*/
-function get_possible_last_child(node, adjacent_only) {
+function get_possible_nested_siblings(node, direction, adjacent_only, seen = new Set()) {
/** @type {Array} */
let fragments = [];
@@ -956,12 +968,24 @@ function get_possible_last_child(node, adjacent_only) {
case 'SlotElement':
fragments.push(node.fragment);
break;
+
+ case 'SnippetBlock':
+ if (seen.has(node)) {
+ return new Map();
+ }
+ seen.add(node);
+ fragments.push(node.body);
+ break;
+
+ case 'Component':
+ fragments.push(node.fragment, ...[...node.metadata.snippets].map((s) => s.body));
+ break;
}
/** @type {Map} NodeMap */
const result = new Map();
- let exhaustive = node.type !== 'SlotElement';
+ let exhaustive = node.type !== 'SlotElement' && node.type !== 'SnippetBlock';
for (const fragment of fragments) {
if (fragment == null) {
@@ -969,7 +993,7 @@ function get_possible_last_child(node, adjacent_only) {
continue;
}
- const map = loop_child(fragment.nodes, adjacent_only);
+ const map = loop_child(fragment.nodes, direction, adjacent_only, seen);
exhaustive &&= has_definite_elements(map);
add_to_map(map, result);
@@ -1012,27 +1036,28 @@ function add_to_map(from, to) {
}
/**
- * @param {NodeExistsValue | undefined} exist1
+ * @param {NodeExistsValue} exist1
* @param {NodeExistsValue | undefined} exist2
* @returns {NodeExistsValue}
*/
function higher_existence(exist1, exist2) {
- // @ts-expect-error TODO figure out if this is a bug
- if (exist1 === undefined || exist2 === undefined) return exist1 || exist2;
+ if (exist2 === undefined) return exist1;
return exist1 > exist2 ? exist1 : exist2;
}
/**
* @param {Compiler.AST.SvelteNode[]} children
+ * @param {Direction} direction
* @param {boolean} adjacent_only
+ * @param {Set} seen
*/
-function loop_child(children, adjacent_only) {
+function loop_child(children, direction, adjacent_only, seen) {
/** @type {Map} */
const result = new Map();
- let i = children.length;
+ let i = direction === FORWARD ? 0 : children.length - 1;
- while (i--) {
+ while (i >= 0 && i < children.length) {
const child = children[i];
if (child.type === 'RegularElement') {
@@ -1042,13 +1067,19 @@ function loop_child(children, adjacent_only) {
}
} else if (child.type === 'SvelteElement') {
result.set(child, NODE_PROBABLY_EXISTS);
+ } else if (child.type === 'RenderTag') {
+ for (const snippet of child.metadata.snippets) {
+ add_to_map(get_possible_nested_siblings(snippet, direction, adjacent_only, seen), result);
+ }
} else if (is_block(child)) {
- const child_result = get_possible_last_child(child, adjacent_only);
+ const child_result = get_possible_nested_siblings(child, direction, adjacent_only, seen);
add_to_map(child_result, result);
if (adjacent_only && has_definite_elements(child_result)) {
break;
}
}
+
+ i = direction === FORWARD ? i + 1 : i - 1;
}
return result;
diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js
index 322293bf6b91..d73e273ec2b5 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/index.js
+++ b/packages/svelte/src/compiler/phases/2-analyze/index.js
@@ -1,12 +1,13 @@
-/** @import { Expression, Node, Program } from 'estree' */
+/** @import { Comment, Expression, Node, Program } from 'estree' */
/** @import { Binding, AST, ValidatedCompileOptions, ValidatedModuleCompileOptions } from '#compiler' */
/** @import { AnalysisState, Visitors } from './types' */
/** @import { Analysis, ComponentAnalysis, Js, ReactiveStatement, Template } from '../types' */
import { walk } from 'zimmerframe';
+import { parse } from '../1-parse/acorn.js';
import * as e from '../../errors.js';
import * as w from '../../warnings.js';
-import { extract_identifiers, is_text_attribute } from '../../utils/ast.js';
-import * as b from '../../utils/builders.js';
+import { extract_identifiers } from '../../utils/ast.js';
+import * as b from '#compiler/builders';
import { Scope, ScopeRoot, create_scopes, get_rune, set_scope } from '../scope.js';
import check_graph_for_cycles from './utils/check_graph_for_cycles.js';
import { create_attribute, is_custom_element_node } from '../nodes.js';
@@ -18,6 +19,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 +45,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 +67,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';
@@ -71,6 +76,7 @@ import { UseDirective } from './visitors/UseDirective.js';
import { VariableDeclarator } from './visitors/VariableDeclarator.js';
import is_reference from 'is-reference';
import { mark_subtree_dynamic } from './visitors/shared/fragment.js';
+import * as state from '../../state.js';
/**
* @type {Visitors}
@@ -131,6 +137,7 @@ const visitors = {
},
ArrowFunctionExpression,
AssignmentExpression,
+ AttachTag,
Attribute,
AwaitBlock,
BindDirective,
@@ -156,9 +163,11 @@ const visitors = {
KeyBlock,
LabeledStatement,
LetDirective,
+ Literal,
MemberExpression,
NewExpression,
OnDirective,
+ PropertyDefinition,
RegularElement,
RenderTag,
SlotElement,
@@ -176,6 +185,7 @@ const visitors = {
SvelteWindow,
SvelteBoundary,
TaggedTemplateExpression,
+ TemplateElement,
Text,
TransitionDirective,
TitleElement,
@@ -223,11 +233,17 @@ function get_component_name(filename) {
const RESERVED = ['$$props', '$$restProps', '$$slots'];
/**
- * @param {Program} ast
+ * @param {string} source
* @param {ValidatedModuleCompileOptions} options
* @returns {Analysis}
*/
-export function analyze_module(ast, options) {
+export function analyze_module(source, options) {
+ /** @type {AST.JSComment[]} */
+ const comments = [];
+
+ state.set_source(source);
+ const ast = parse(source, comments, false, false);
+
const { scope, scopes } = create_scopes(ast, new ScopeRoot(), false, null);
for (const [name, references] of scope.references) {
@@ -250,16 +266,25 @@ export function analyze_module(ast, options) {
accessors: false,
runes: true,
immutable: true,
- tracing: false
+ tracing: false,
+ comments,
+ classes: new Map()
};
+ state.reset({
+ dev: options.dev,
+ filename: options.filename,
+ rootDir: options.rootDir,
+ runes: true
+ });
+
walk(
/** @type {Node} */ (ast),
{
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),
@@ -420,9 +445,34 @@ export function analyze_component(root, source, options) {
module,
instance,
template,
+ comments: root.comments,
elements: [],
runes,
+ // if we are not in runes mode but we have no reserved references ($$props, $$restProps)
+ // and no `export let` we might be in a wannabe runes component that is using runes in an external
+ // module...we need to fallback to the runic behavior
+ maybe_runes:
+ !runes &&
+ // if they explicitly disabled runes, use the legacy behavior
+ options.runes !== false &&
+ ![...module.scope.references.keys()].some((name) =>
+ ['$$props', '$$restProps'].includes(name)
+ ) &&
+ !instance.ast.body.some(
+ (node) =>
+ node.type === 'LabeledStatement' ||
+ (node.type === 'ExportNamedDeclaration' &&
+ ((node.declaration &&
+ node.declaration.type === 'VariableDeclaration' &&
+ node.declaration.kind === 'let') ||
+ node.specifiers.some(
+ (specifier) =>
+ specifier.local.type === 'Identifier' &&
+ instance.scope.get(specifier.local.name)?.declaration_kind === 'let'
+ )))
+ ),
tracing: false,
+ classes: new Map(),
immutable: runes || options.immutable,
exports: [],
uses_props: false,
@@ -432,6 +482,7 @@ export function analyze_component(root, source, options) {
uses_component_bindings: false,
uses_render_tags: false,
needs_context: false,
+ needs_mutation_validation: false,
needs_props: false,
event_directive_node: null,
uses_event_attributes: false,
@@ -455,7 +506,8 @@ export function analyze_component(root, source, options) {
hash
})
: '',
- keyframes: []
+ keyframes: [],
+ has_global: false
},
source,
undefined_exports: new Map(),
@@ -463,6 +515,14 @@ export function analyze_component(root, source, options) {
snippets: new Set()
};
+ state.reset({
+ component_name: analysis.name,
+ dev: options.dev,
+ filename: options.filename,
+ rootDir: options.rootDir,
+ runes: true
+ });
+
if (!runes) {
// every exported `let` or `var` declaration becomes a prop, everything else becomes an export
for (const node of instance.ast.body) {
@@ -616,7 +676,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
};
@@ -683,7 +743,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
};
@@ -769,17 +829,24 @@ export function analyze_component(root, source, options) {
}
let has_class = false;
+ let has_style = false;
let has_spread = false;
let has_class_directive = false;
+ let has_style_directive = false;
for (const attribute of node.attributes) {
// The spread method appends the hash to the end of the class attribute on its own
if (attribute.type === 'SpreadAttribute') {
has_spread = true;
break;
+ } else if (attribute.type === 'Attribute') {
+ has_class ||= attribute.name.toLowerCase() === 'class';
+ has_style ||= attribute.name.toLowerCase() === 'style';
+ } else if (attribute.type === 'ClassDirective') {
+ has_class_directive = true;
+ } else if (attribute.type === 'StyleDirective') {
+ has_style_directive = true;
}
- has_class_directive ||= attribute.type === 'ClassDirective';
- has_class ||= attribute.type === 'Attribute' && attribute.name.toLowerCase() === 'class';
}
// We need an empty class to generate the set_class() or class="" correctly
@@ -796,6 +863,21 @@ export function analyze_component(root, source, options) {
])
);
}
+
+ // We need an empty style to generate the set_style() correctly
+ if (!has_spread && !has_style && has_style_directive) {
+ node.attributes.push(
+ create_attribute('style', -1, -1, [
+ {
+ type: 'Text',
+ data: '',
+ raw: '',
+ start: -1,
+ end: -1
+ }
+ ])
+ );
+ }
}
// TODO
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 17c8123de1fe..080239bac063 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 a64c89cd88f1..39358f72fc1b 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;
@@ -23,5 +23,9 @@ export function AssignmentExpression(node, context) {
}
}
+ if (context.state.expression) {
+ context.state.expression.has_assignment = true;
+ }
+
context.next();
}
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 000000000000..1e318f228d8b
--- /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 561a00452684..773aa597444b 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/visitors/Attribute.js
+++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/Attribute.js
@@ -162,16 +162,8 @@ function get_delegated_event(event_name, handler, context) {
return unhoisted;
}
- if (binding !== null && binding.initial !== null && !binding.updated) {
- const binding_type = binding.initial.type;
-
- if (
- binding_type === 'ArrowFunctionExpression' ||
- binding_type === 'FunctionDeclaration' ||
- binding_type === 'FunctionExpression'
- ) {
- target_function = binding.initial;
- }
+ if (binding?.is_function()) {
+ target_function = binding.initial;
}
}
@@ -191,6 +183,15 @@ function get_delegated_event(event_name, handler, context) {
const binding = scope.get(reference);
const local_binding = context.state.scope.get(reference);
+ // if the function access a snippet that can't be hoisted we bail out
+ if (
+ local_binding !== null &&
+ local_binding.initial?.type === 'SnippetBlock' &&
+ !local_binding.initial.metadata.can_hoist
+ ) {
+ return unhoisted;
+ }
+
// If we are referencing a binding that is shadowed in another scope then bail out.
if (local_binding !== null && binding !== null && local_binding.node !== binding.node) {
return unhoisted;
@@ -210,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/AwaitBlock.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitBlock.js
index a71f325154ff..5aa04ba3b9a8 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitBlock.js
+++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitBlock.js
@@ -41,5 +41,8 @@ export function AwaitBlock(node, context) {
mark_subtree_dynamic(context.path);
- context.next();
+ context.visit(node.expression, { ...context.state, expression: node.metadata.expression });
+ if (node.pending) context.visit(node.pending);
+ if (node.then) context.visit(node.then);
+ if (node.catch) context.visit(node.catch);
}
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 18ea79262b50..9f02e7fa5a02 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 4d09d9293fb2..33abb52cac5c 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js
+++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js
@@ -3,10 +3,10 @@
/** @import { Context } from '../types' */
import { get_rune } from '../../scope.js';
import * as e from '../../../errors.js';
-import { get_parent, unwrap_optional } from '../../../utils/ast.js';
+import { get_parent } from '../../../utils/ast.js';
import { is_pure, is_safe_identifier } from './shared/utils.js';
import { dev, locate_node, source } from '../../../state.js';
-import * as b from '../../../utils/builders.js';
+import * as b from '#compiler/builders';
/**
* @param {CallExpression} node
@@ -17,6 +17,14 @@ export function CallExpression(node, context) {
const rune = get_rune(node, context.state.scope);
+ if (rune && rune !== '$inspect') {
+ for (const arg of node.arguments) {
+ if (arg.type === 'SpreadElement') {
+ e.rune_invalid_spread(node, rune);
+ }
+ }
+ }
+
switch (rune) {
case null:
if (!is_safe_identifier(node.callee, context.state.scope)) {
@@ -42,6 +50,9 @@ export function CallExpression(node, context) {
e.bindable_invalid_location(node);
}
+ // We need context in case the bound prop is stale
+ context.state.analysis.needs_context = true;
+
break;
case '$host':
@@ -103,22 +114,24 @@ 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);
}
if ((rune === '$derived' || rune === '$derived.by') && node.arguments.length !== 1) {
e.rune_invalid_arguments_length(node, rune, 'exactly one argument');
- } else if (rune === '$state' && node.arguments.length > 1) {
+ } else if (node.arguments.length > 1) {
e.rune_invalid_arguments_length(node, rune, 'zero or one arguments');
}
break;
+ }
case '$effect':
case '$effect.pre':
@@ -259,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 0463e4da8563..ffc39ac00de1 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/ConstTag.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/ConstTag.js
index f723f8447cd2..d5f5f7b2e0a0 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/visitors/ConstTag.js
+++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/ConstTag.js
@@ -32,5 +32,8 @@ export function ConstTag(node, context) {
e.const_tag_invalid_placement(node);
}
- context.next();
+ const declaration = node.declaration.declarations[0];
+
+ context.visit(declaration.id);
+ context.visit(declaration.init, { ...context.state, expression: node.metadata.expression });
}
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 bd6c936f99a7..e6a83921b1f1 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/ExportNamedDeclaration.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/ExportNamedDeclaration.js
index 547f6ab9c73e..4b85894e5250 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/visitors/ExportNamedDeclaration.js
+++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/ExportNamedDeclaration.js
@@ -1,7 +1,5 @@
-/** @import { ExportNamedDeclaration, Identifier, Node } from 'estree' */
-/** @import { Binding } from '#compiler' */
+/** @import { ExportNamedDeclaration, Identifier } from 'estree' */
/** @import { Context } from '../types' */
-/** @import { Scope } from '../../scope' */
import * as e from '../../../errors.js';
import { extract_identifiers } from '../../../utils/ast.js';
diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/HtmlTag.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/HtmlTag.js
index c89b11ad3695..7b0e501760f0 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/visitors/HtmlTag.js
+++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/HtmlTag.js
@@ -15,5 +15,5 @@ export function HtmlTag(node, context) {
// unfortunately this is necessary in order to fix invalid HTML
mark_subtree_dynamic(context.path);
- context.next();
+ context.next({ ...context.state, expression: node.metadata.expression });
}
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 79dccd5a7cf5..cced326f9baa 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/visitors/Identifier.js
+++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/Identifier.js
@@ -1,5 +1,4 @@
/** @import { Expression, Identifier } from 'estree' */
-/** @import { EachBlock } from '#compiler' */
/** @import { Context } from '../types' */
import is_reference from 'is-reference';
import { should_proxy } from '../../3-transform/client/utils.js';
@@ -7,6 +6,7 @@ import * as e from '../../../errors.js';
import * as w from '../../../warnings.js';
import { is_rune } from '../../../../utils.js';
import { mark_subtree_dynamic } from './shared/fragment.js';
+import { get_rune } from '../../scope.js';
/**
* @param {Identifier} node
@@ -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,11 @@ 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.references.add(binding);
+ context.state.expression.has_state ||=
+ binding.kind !== 'static' &&
+ !binding.is_function() &&
+ !context.state.scope.evaluate(node).is_known;
}
if (
@@ -111,7 +115,34 @@ export function Identifier(node, context) {
(parent.type !== 'AssignmentExpression' || parent.left !== node) &&
parent.type !== 'UpdateExpression'
) {
- w.state_referenced_locally(node);
+ let type = 'closure';
+
+ let i = context.path.length;
+ while (i--) {
+ const parent = context.path[i];
+
+ if (
+ parent.type === 'ArrowFunctionExpression' ||
+ parent.type === 'FunctionDeclaration' ||
+ parent.type === 'FunctionExpression'
+ ) {
+ break;
+ }
+
+ if (
+ parent.type === 'CallExpression' &&
+ parent.arguments.includes(/** @type {any} */ (context.path[i + 1]))
+ ) {
+ const rune = get_rune(parent, context.state.scope);
+
+ if (rune === '$state' || rune === '$state.raw') {
+ type = 'derived';
+ break;
+ }
+ }
+ }
+
+ w.state_referenced_locally(node, node.name, type);
}
if (
diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/IfBlock.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/IfBlock.js
index a65771bcfca9..dcdae3587f63 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/visitors/IfBlock.js
+++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/IfBlock.js
@@ -17,5 +17,11 @@ export function IfBlock(node, context) {
mark_subtree_dynamic(context.path);
- context.next();
+ context.visit(node.test, {
+ ...context.state,
+ expression: node.metadata.expression
+ });
+
+ context.visit(node.consequent);
+ if (node.alternate) context.visit(node.alternate);
}
diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/KeyBlock.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/KeyBlock.js
index 88bb6a98e748..09e604ea66be 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/visitors/KeyBlock.js
+++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/KeyBlock.js
@@ -16,5 +16,6 @@ export function KeyBlock(node, context) {
mark_subtree_dynamic(context.path);
- context.next();
+ context.visit(node.expression, { ...context.state, expression: node.metadata.expression });
+ context.visit(node.fragment);
}
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 000000000000..58684ba71ca0
--- /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/MemberExpression.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/MemberExpression.js
index 6ea8f238e150..0a3b3861986c 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/visitors/MemberExpression.js
+++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/MemberExpression.js
@@ -1,10 +1,7 @@
-/** @import { MemberExpression, Node } from 'estree' */
+/** @import { MemberExpression } from 'estree' */
/** @import { Context } from '../types' */
import * as e from '../../../errors.js';
-import * as w from '../../../warnings.js';
-import { object } from '../../../utils/ast.js';
import { is_pure, is_safe_identifier } from './shared/utils.js';
-import { mark_subtree_dynamic } from './shared/fragment.js';
/**
* @param {MemberExpression} node
@@ -18,8 +15,9 @@ export function MemberExpression(node, context) {
}
}
- if (context.state.expression && !is_pure(node, context)) {
- context.state.expression.has_state = true;
+ if (context.state.expression) {
+ context.state.expression.has_member_expression = true;
+ context.state.expression.has_state ||= !is_pure(node, context);
}
if (!is_safe_identifier(node, context.state.scope)) {
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 000000000000..99d05cb47c0d
--- /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/RegularElement.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/RegularElement.js
index 03dfaebcb793..d5689e5d5592 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/visitors/RegularElement.js
+++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/RegularElement.js
@@ -173,7 +173,8 @@ export function RegularElement(node, context) {
if (
context.state.analysis.source[node.end - 2] === '/' &&
!is_void(node_name) &&
- !is_svg(node_name)
+ !is_svg(node_name) &&
+ !is_mathml(node_name)
) {
w.element_invalid_self_closing_tag(node, node.name);
}
diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/RenderTag.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/RenderTag.js
index a8c9d408bdad..1230ef6b048c 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/visitors/RenderTag.js
+++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/RenderTag.js
@@ -54,7 +54,7 @@ export function RenderTag(node, context) {
mark_subtree_dynamic(context.path);
- context.visit(callee);
+ context.visit(callee, { ...context.state, expression: node.metadata.expression });
for (const arg of expression.arguments) {
const metadata = create_expression_metadata();
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 000000000000..978042bbc589
--- /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 363a111b7dc6..a03421e8dd26 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 741effc67a0d..ed48e026ac65 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;
@@ -21,5 +21,9 @@ export function UpdateExpression(node, context) {
}
}
+ if (context.state.expression) {
+ context.state.expression.has_assignment = true;
+ }
+
context.next();
}
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 a7d08d315d8f..89320f396267 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 04bf3d2ff3bf..aca87fab811c 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/function.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/function.js
index c892efd421d1..177616785026 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/function.js
+++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/function.js
@@ -13,6 +13,16 @@ export function visit_function(node, context) {
scope: context.state.scope
};
+ if (context.state.expression) {
+ for (const [name] of context.state.scope.references) {
+ const binding = context.state.scope.get(name);
+
+ if (binding && binding.scope.function_depth < context.state.scope.function_depth) {
+ context.state.expression.references.add(binding);
+ }
+ }
+ }
+
context.next({
...context.state,
function_depth: context.state.function_depth + 1,
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 04f4347a40bb..d7b682da08aa 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
@@ -1,31 +1,28 @@
-/** @import { AssignmentExpression, Expression, Identifier, Literal, Node, Pattern, PrivateIdentifier, Super, UpdateExpression, VariableDeclarator } from 'estree' */
+/** @import { AssignmentExpression, Expression, Literal, Node, Pattern, Super, UpdateExpression, VariableDeclarator } from 'estree' */
/** @import { AST, Binding } from '#compiler' */
/** @import { AnalysisState, Context } from '../../types' */
/** @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 '../../../../utils/builders.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?.kind === 'derived') {
- e.constant_assignment(node, 'derived state');
- }
-
- 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()');
}
@@ -38,24 +35,40 @@ export function validate_assignment(node, argument, state) {
e.snippet_parameter_assignment(node);
}
}
- if (
- argument.type === 'MemberExpression' &&
- argument.object.type === 'ThisExpression' &&
- (((argument.property.type === 'PrivateIdentifier' || argument.property.type === 'Identifier') &&
- state.derived_state.some(
- (derived) =>
- derived.name === /** @type {PrivateIdentifier | Identifier} */ (argument.property).name &&
- derived.private === (argument.property.type === 'PrivateIdentifier')
- )) ||
- (argument.property.type === 'Literal' &&
- argument.property.value &&
- typeof argument.property.value === 'string' &&
- state.derived_state.some(
- (derived) =>
- derived.name === /** @type {Literal} */ (argument.property).value && !derived.private
- )))
- ) {
- e.constant_assignment(node, 'derived state');
+
+ 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;
+ }
+ }
+ }
}
}
@@ -81,7 +94,6 @@ export function validate_no_const_assignment(node, argument, scope, is_binding)
} else if (argument.type === 'Identifier') {
const binding = scope.get(argument.name);
if (
- binding?.kind === 'derived' ||
binding?.declaration_kind === 'import' ||
(binding?.declaration_kind === 'const' && binding.kind !== 'each')
) {
@@ -96,12 +108,7 @@ export function validate_no_const_assignment(node, argument, scope, is_binding)
// );
// TODO have a more specific error message for assignments to things like `{:then foo}`
- const thing =
- binding.declaration_kind === 'import'
- ? 'import'
- : binding.kind === 'derived'
- ? 'derived state'
- : 'constant';
+ const thing = binding.declaration_kind === 'import' ? 'import' : 'constant';
if (is_binding) {
e.constant_binding(node, thing);
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 cf5ba285cbf3..a9c0651e0f4e 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
@@ -3,7 +3,7 @@
/** @import { ComponentAnalysis, Analysis } from '../../types' */
/** @import { Visitors, ComponentClientTransformState, ClientTransformState } from './types' */
import { walk } from 'zimmerframe';
-import * as b from '../../../utils/builders.js';
+import * as b from '#compiler/builders';
import { build_getter, is_state_source } from './utils.js';
import { render_stylesheet } from '../css/index.js';
import { dev, filename } from '../../../state.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: [],
@@ -171,10 +168,9 @@ export function client_component(analysis, options) {
// these are set inside the `Fragment` visitor, and cannot be used until then
init: /** @type {any} */ (null),
update: /** @type {any} */ (null),
- expressions: /** @type {any} */ (null),
after_update: /** @type {any} */ (null),
template: /** @type {any} */ (null),
- locations: /** @type {any} */ (null)
+ memoizer: /** @type {any} */ (null)
};
const module = /** @type {ESTree.Program} */ (
@@ -219,7 +215,10 @@ export function client_component(analysis, options) {
for (const [name, binding] of analysis.instance.scope.declarations) {
if (binding.kind === 'legacy_reactive') {
legacy_reactive_declarations.push(
- b.const(name, b.call('$.mutable_state', undefined, analysis.immutable ? b.true : undefined))
+ b.const(
+ name,
+ b.call('$.mutable_source', undefined, analysis.immutable ? b.true : undefined)
+ )
);
}
if (binding.kind === 'store_sub') {
@@ -363,6 +362,9 @@ export function client_component(analysis, options) {
.../** @type {ESTree.Statement[]} */ (template.body)
]);
+ // trick esrap into including comments
+ component_block.loc = instance.loc;
+
if (!analysis.runes) {
// Bind static exports to props so that people can access them with bind:x
for (const { name, alias } of analysis.exports) {
@@ -390,6 +392,12 @@ export function client_component(analysis, options) {
);
}
+ if (analysis.needs_mutation_validation) {
+ component_block.body.unshift(
+ b.var('$$ownership_validator', b.call('$.create_ownership_validator', b.id('$$props')))
+ );
+ }
+
const should_inject_context =
dev ||
analysis.needs_context ||
@@ -527,9 +535,6 @@ export function client_component(analysis, options) {
b.assignment('=', b.member(b.id(analysis.name), '$.FILENAME', true), b.literal(filename))
)
);
-
- body.unshift(b.stmt(b.call(b.id('$.mark_module_start'))));
- body.push(b.stmt(b.call(b.id('$.mark_module_end'), b.id(analysis.name))));
}
if (!analysis.runes) {
@@ -596,7 +601,7 @@ export function client_component(analysis, options) {
/** @type {ESTree.Property[]} */ (
[
prop_def.attribute ? b.init('attribute', b.literal(prop_def.attribute)) : undefined,
- prop_def.reflect ? b.init('reflect', b.literal(true)) : undefined,
+ prop_def.reflect ? b.init('reflect', b.true) : undefined,
prop_def.type ? b.init('type', b.literal(prop_def.type)) : undefined
].filter(Boolean)
)
@@ -663,8 +668,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 000000000000..ce56c43d7c50
--- /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 000000000000..d0327e702ad5
--- /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 000000000000..8f7f8a1f4357
--- /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 `
+ * ```
+ */
+export function getAbortSignal() {
+ if (active_reaction === null) {
+ e.get_abort_signal_outside_reaction();
+ }
+
+ return (active_reaction.ac ??= new AbortController()).signal;
+}
+
+/**
+ * `onMount`, like [`$effect`](https://svelte.dev/docs/svelte/$effect), schedules a function to run as soon as the component has been mounted to the DOM.
+ * Unlike `$effect`, the provided function only runs once.
+ *
+ * It must be called during the component's initialisation (but doesn't need to live _inside_ the component;
+ * it can be called from an external module). If a function is returned _synchronously_ from `onMount`,
+ * it will be called when the component is unmounted.
+ *
+ * `onMount` functions do not run during [server-side rendering](https://svelte.dev/docs/svelte/svelte-server#render).
*
* @template T
* @param {() => NotFunction | Promise> | (() => any)} fn
@@ -113,7 +145,7 @@ function create_custom_event(type, detail, { bubbles = false, cancelable = false
* The event dispatcher can be typed to narrow the allowed event names and the type of the `detail` argument:
* ```ts
* const dispatch = createEventDispatcher<{
- * loaded: never; // does not take a detail argument
+ * loaded: null; // does not take a detail argument
* change: string; // takes a detail argument of type string, which is required
* optional: number | null; // takes an optional detail argument of type number
* }>();
diff --git a/packages/svelte/src/index-server.js b/packages/svelte/src/index-server.js
index 0f1aff8f5aa7..e5039cf15092 100644
--- a/packages/svelte/src/index-server.js
+++ b/packages/svelte/src/index-server.js
@@ -35,6 +35,8 @@ export function unmount() {
export async function tick() {}
+export { getAbortSignal } from './internal/server/abort-signal.js';
+
export { getAllContexts, getContext, hasContext, setContext } from './internal/server/context.js';
export { createRawSnippet } from './internal/server/blocks/snippet.js';
diff --git a/packages/svelte/src/internal/client/constants.js b/packages/svelte/src/internal/client/constants.js
index a4840ce4ebd0..1cffa4394081 100644
--- a/packages/svelte/src/internal/client/constants.js
+++ b/packages/svelte/src/internal/client/constants.js
@@ -15,13 +15,23 @@ export const DESTROYED = 1 << 14;
export const EFFECT_RAN = 1 << 15;
/** 'Transparent' effects do not create a transition boundary */
export const EFFECT_TRANSPARENT = 1 << 16;
-/** Svelte 4 legacy mode props need to be handled with deriveds and be recognized elsewhere, hence the dedicated flag */
-export const LEGACY_DERIVED_PROP = 1 << 17;
-export const INSPECT_EFFECT = 1 << 18;
-export const HEAD_EFFECT = 1 << 19;
-export const EFFECT_HAS_DERIVED = 1 << 20;
+export const INSPECT_EFFECT = 1 << 17;
+export const HEAD_EFFECT = 1 << 18;
+export const EFFECT_PRESERVED = 1 << 19;
+export const EFFECT_IS_UPDATING = 1 << 20;
export const STATE_SYMBOL = Symbol('$state');
-export const STATE_SYMBOL_METADATA = Symbol('$state metadata');
export const LEGACY_PROPS = Symbol('legacy props');
export const LOADING_ATTR_SYMBOL = Symbol('');
+export const PROXY_PATH_SYMBOL = Symbol('proxy path');
+
+/** allow users to ignore aborted signal errors if `reason.name === 'StaleReactionError` */
+export const STALE_REACTION = new (class StaleReactionError extends Error {
+ name = 'StaleReactionError';
+ message = 'The reaction that called `getAbortSignal()` was re-run or destroyed';
+})();
+
+export const ELEMENT_NODE = 1;
+export const TEXT_NODE = 3;
+export const COMMENT_NODE = 8;
+export const DOCUMENT_FRAGMENT_NODE = 11;
diff --git a/packages/svelte/src/internal/client/context.js b/packages/svelte/src/internal/client/context.js
index bd94d5ad8a19..e4220149ab2c 100644
--- a/packages/svelte/src/internal/client/context.js
+++ b/packages/svelte/src/internal/client/context.js
@@ -1,18 +1,17 @@
-/** @import { ComponentContext } from '#client' */
+/** @import { ComponentContext, DevStackEntry } from '#client' */
import { DEV } from 'esm-env';
-import { add_owner } from './dev/ownership.js';
import { lifecycle_outside_component } from '../shared/errors.js';
import { source } from './reactivity/sources.js';
import {
active_effect,
active_reaction,
set_active_effect,
- set_active_reaction,
- untrack
+ set_active_reaction
} from './runtime.js';
-import { effect } from './reactivity/effects.js';
+import { effect, teardown } from './reactivity/effects.js';
import { legacy_mode_flag } from '../flags/index.js';
+import { FILENAME } from '../../constants.js';
/** @type {ComponentContext | null} */
export let component_context = null;
@@ -22,6 +21,43 @@ export function set_component_context(context) {
component_context = context;
}
+/** @type {DevStackEntry | null} */
+export let dev_stack = null;
+
+/** @param {DevStackEntry | null} stack */
+export function set_dev_stack(stack) {
+ dev_stack = stack;
+}
+
+/**
+ * Execute a callback with a new dev stack entry
+ * @param {() => any} callback - Function to execute
+ * @param {DevStackEntry['type']} type - Type of block/component
+ * @param {any} component - Component function
+ * @param {number} line - Line number
+ * @param {number} column - Column number
+ * @param {Record} [additional] - Any additional properties to add to the dev stack entry
+ * @returns {any}
+ */
+export function add_svelte_meta(callback, type, component, line, column, additional) {
+ const parent = dev_stack;
+
+ dev_stack = {
+ type,
+ file: component[FILENAME],
+ line,
+ column,
+ parent,
+ ...additional
+ };
+
+ try {
+ return callback();
+ } finally {
+ dev_stack = parent;
+ }
+}
+
/**
* The current component function. Different from current component context:
* ```html
@@ -67,15 +103,6 @@ export function getContext(key) {
*/
export function setContext(key, context) {
const context_map = get_or_init_context_map('setContext');
-
- if (DEV) {
- // When state is put into context, we treat as if it's global from now on.
- // We do for performance reasons (it's for example very expensive to call
- // getContext on a big object many times when part of a list component)
- // and danger of false positives.
- untrack(() => add_owner(context, null, true));
- }
-
context_map.set(key, context);
return context;
}
@@ -112,15 +139,16 @@ export function getAllContexts() {
* @returns {void}
*/
export function push(props, runes = false, fn) {
- component_context = {
+ var ctx = (component_context = {
p: component_context,
c: null,
+ d: false,
e: null,
m: false,
s: props,
x: null,
l: null
- };
+ });
if (legacy_mode_flag && !runes) {
component_context.l = {
@@ -131,6 +159,10 @@ export function push(props, runes = false, fn) {
};
}
+ teardown(() => {
+ /** @type {ComponentContext} */ (ctx).d = true;
+ });
+
if (DEV) {
// component function
component_context.function = fn;
diff --git a/packages/svelte/src/internal/client/dev/console-log.js b/packages/svelte/src/internal/client/dev/console-log.js
index a578ecea452c..d314359ef615 100644
--- a/packages/svelte/src/internal/client/dev/console-log.js
+++ b/packages/svelte/src/internal/client/dev/console-log.js
@@ -1,4 +1,4 @@
-import { STATE_SYMBOL } from '../constants.js';
+import { STATE_SYMBOL } from '#client/constants';
import { snapshot } from '../../shared/clone.js';
import * as w from '../warnings.js';
import { untrack } from '../runtime.js';
diff --git a/packages/svelte/src/internal/client/dev/debug.js b/packages/svelte/src/internal/client/dev/debug.js
new file mode 100644
index 000000000000..fbde87a2d764
--- /dev/null
+++ b/packages/svelte/src/internal/client/dev/debug.js
@@ -0,0 +1,109 @@
+/** @import { Derived, Effect, Value } from '#client' */
+
+import {
+ BLOCK_EFFECT,
+ BOUNDARY_EFFECT,
+ BRANCH_EFFECT,
+ CLEAN,
+ DERIVED,
+ EFFECT,
+ MAYBE_DIRTY,
+ RENDER_EFFECT,
+ ROOT_EFFECT
+} from '#client/constants';
+
+/**
+ *
+ * @param {Effect} effect
+ */
+export function root(effect) {
+ while (effect.parent !== null) {
+ effect = effect.parent;
+ }
+
+ return effect;
+}
+
+/**
+ *
+ * @param {Effect} effect
+ */
+export function log_effect_tree(effect, depth = 0) {
+ const flags = effect.f;
+
+ let label = '(unknown)';
+
+ if ((flags & ROOT_EFFECT) !== 0) {
+ label = 'root';
+ } else if ((flags & BOUNDARY_EFFECT) !== 0) {
+ label = 'boundary';
+ } else if ((flags & BLOCK_EFFECT) !== 0) {
+ label = 'block';
+ } else if ((flags & BRANCH_EFFECT) !== 0) {
+ label = 'branch';
+ } else if ((flags & RENDER_EFFECT) !== 0) {
+ label = 'render effect';
+ } else if ((flags & EFFECT) !== 0) {
+ label = 'effect';
+ }
+
+ let status =
+ (flags & CLEAN) !== 0 ? 'clean' : (flags & MAYBE_DIRTY) !== 0 ? 'maybe dirty' : 'dirty';
+
+ // eslint-disable-next-line no-console
+ console.group(`%c${label} (${status})`, `font-weight: ${status === 'clean' ? 'normal' : 'bold'}`);
+
+ if (depth === 0) {
+ const callsite = new Error().stack
+ ?.split('\n')[2]
+ .replace(/\s+at (?: \w+\(?)?(.+)\)?/, (m, $1) => $1.replace(/\?[^:]+/, ''));
+
+ // eslint-disable-next-line no-console
+ console.log(callsite);
+ }
+
+ if (effect.deps !== null) {
+ // eslint-disable-next-line no-console
+ console.groupCollapsed('%cdeps', 'font-weight: normal');
+
+ for (const dep of effect.deps) {
+ log_dep(dep);
+ }
+
+ // eslint-disable-next-line no-console
+ console.groupEnd();
+ }
+
+ let child = effect.first;
+ while (child !== null) {
+ log_effect_tree(child, depth + 1);
+ child = child.next;
+ }
+
+ // eslint-disable-next-line no-console
+ console.groupEnd();
+}
+
+/**
+ *
+ * @param {Value} dep
+ */
+function log_dep(dep) {
+ if ((dep.f & DERIVED) !== 0) {
+ const derived = /** @type {Derived} */ (dep);
+
+ // eslint-disable-next-line no-console
+ console.groupCollapsed('%cderived', 'font-weight: normal', derived.v);
+ if (derived.deps) {
+ for (const d of derived.deps) {
+ log_dep(d);
+ }
+ }
+
+ // eslint-disable-next-line no-console
+ console.groupEnd();
+ } else {
+ // eslint-disable-next-line no-console
+ console.log('state', dep.v);
+ }
+}
diff --git a/packages/svelte/src/internal/client/dev/elements.js b/packages/svelte/src/internal/client/dev/elements.js
index bf75e6e5ccef..8dd54e0a2ac5 100644
--- a/packages/svelte/src/internal/client/dev/elements.js
+++ b/packages/svelte/src/internal/client/dev/elements.js
@@ -1,6 +1,8 @@
-/** @import { SourceLocation } from '#shared' */
+/** @import { SourceLocation } from '#client' */
+import { COMMENT_NODE, DOCUMENT_FRAGMENT_NODE, ELEMENT_NODE } from '#client/constants';
import { HYDRATION_END, HYDRATION_START, HYDRATION_START_ELSE } from '../../../constants.js';
import { hydrating } from '../dom/hydration.js';
+import { dev_stack } from '../context.js';
/**
* @param {any} fn
@@ -12,7 +14,7 @@ export function add_locations(fn, filename, locations) {
return (/** @type {any[]} */ ...args) => {
const dom = fn(...args);
- var node = hydrating ? dom : dom.nodeType === 11 ? dom.firstChild : dom;
+ var node = hydrating ? dom : dom.nodeType === DOCUMENT_FRAGMENT_NODE ? dom.firstChild : dom;
assign_locations(node, filename, locations);
return dom;
@@ -27,6 +29,7 @@ export function add_locations(fn, filename, locations) {
function assign_location(element, filename, location) {
// @ts-expect-error
element.__svelte_meta = {
+ parent: dev_stack,
loc: { file: filename, line: location[0], column: location[1] }
};
@@ -45,13 +48,13 @@ function assign_locations(node, filename, locations) {
var depth = 0;
while (node && i < locations.length) {
- if (hydrating && node.nodeType === 8) {
+ if (hydrating && node.nodeType === COMMENT_NODE) {
var comment = /** @type {Comment} */ (node);
if (comment.data === HYDRATION_START || comment.data === HYDRATION_START_ELSE) depth += 1;
else if (comment.data[0] === HYDRATION_END) depth -= 1;
}
- if (depth === 0 && node.nodeType === 1) {
+ if (depth === 0 && node.nodeType === ELEMENT_NODE) {
assign_location(/** @type {Element} */ (node), filename, locations[i++]);
}
diff --git a/packages/svelte/src/internal/client/dev/hmr.js b/packages/svelte/src/internal/client/dev/hmr.js
index ee5e08c0b14a..27e2643d1674 100644
--- a/packages/svelte/src/internal/client/dev/hmr.js
+++ b/packages/svelte/src/internal/client/dev/hmr.js
@@ -1,6 +1,6 @@
/** @import { Source, Effect, TemplateNode } from '#client' */
import { FILENAME, HMR } from '../../../constants.js';
-import { EFFECT_TRANSPARENT } from '../constants.js';
+import { EFFECT_TRANSPARENT } from '#client/constants';
import { hydrate_node, hydrating } from '../dom/hydration.js';
import { block, branch, destroy_effect } from '../reactivity/effects.js';
import { source } from '../reactivity/sources.js';
diff --git a/packages/svelte/src/internal/client/dev/inspect.js b/packages/svelte/src/internal/client/dev/inspect.js
index e13ef470cf50..e15c66901c5f 100644
--- a/packages/svelte/src/internal/client/dev/inspect.js
+++ b/packages/svelte/src/internal/client/dev/inspect.js
@@ -1,6 +1,7 @@
import { UNINITIALIZED } from '../../../constants.js';
import { snapshot } from '../../shared/clone.js';
import { inspect_effect, validate_effect } from '../reactivity/effects.js';
+import { untrack } from '../runtime.js';
/**
* @param {() => any[]} get_value
@@ -28,7 +29,10 @@ export function inspect(get_value, inspector = console.log) {
}
if (value !== UNINITIALIZED) {
- inspector(initial ? 'init' : 'update', ...snapshot(value, true));
+ var snap = snapshot(value, true);
+ untrack(() => {
+ inspector(initial ? 'init' : 'update', ...snap);
+ });
}
initial = false;
diff --git a/packages/svelte/src/internal/client/dev/legacy.js b/packages/svelte/src/internal/client/dev/legacy.js
index 138213c5517b..02428dc82457 100644
--- a/packages/svelte/src/internal/client/dev/legacy.js
+++ b/packages/svelte/src/internal/client/dev/legacy.js
@@ -1,7 +1,6 @@
import * as e from '../errors.js';
import { component_context } from '../context.js';
import { FILENAME } from '../../../constants.js';
-import { get_component } from './ownership.js';
/** @param {Function & { [FILENAME]: string }} target */
export function check_target(target) {
@@ -15,9 +14,7 @@ export function legacy_api() {
/** @param {string} method */
function error(method) {
- // @ts-expect-error
- const parent = get_component()?.[FILENAME] ?? 'Something';
- e.component_api_changed(parent, method, component[FILENAME]);
+ e.component_api_changed(method, component[FILENAME]);
}
return {
diff --git a/packages/svelte/src/internal/client/dev/ownership.js b/packages/svelte/src/internal/client/dev/ownership.js
index 62119b36dbd6..19d2cdb34348 100644
--- a/packages/svelte/src/internal/client/dev/ownership.js
+++ b/packages/svelte/src/internal/client/dev/ownership.js
@@ -1,304 +1,81 @@
-/** @import { ProxyMetadata } from '#client' */
/** @typedef {{ file: string, line: number, column: number }} Location */
-import { STATE_SYMBOL_METADATA } from '../constants.js';
-import { render_effect, user_pre_effect } from '../reactivity/effects.js';
-import { dev_current_component_function } from '../context.js';
-import { get_prototype_of } from '../../shared/utils.js';
+import { get_descriptor } from '../../shared/utils.js';
+import { LEGACY_PROPS, STATE_SYMBOL } from '#client/constants';
+import { FILENAME } from '../../../constants.js';
+import { component_context } from '../context.js';
import * as w from '../warnings.js';
-import { FILENAME, UNINITIALIZED } from '../../../constants.js';
-
-/** @type {Record>} */
-const boundaries = {};
-
-const chrome_pattern = /at (?:.+ \()?(.+):(\d+):(\d+)\)?$/;
-const firefox_pattern = /@(.+):(\d+):(\d+)$/;
-
-function get_stack() {
- const stack = new Error().stack;
- if (!stack) return null;
-
- const entries = [];
-
- for (const line of stack.split('\n')) {
- let match = chrome_pattern.exec(line) ?? firefox_pattern.exec(line);
-
- if (match) {
- entries.push({
- file: match[1],
- line: +match[2],
- column: +match[3]
- });
- }
- }
-
- return entries;
-}
+import { sanitize_location } from '../../../utils.js';
/**
- * Determines which `.svelte` component is responsible for a given state change
- * @returns {Function | null}
+ * Sets up a validator that
+ * - traverses the path of a prop to find out if it is allowed to be mutated
+ * - checks that the binding chain is not interrupted
+ * @param {Record} props
*/
-export function get_component() {
- // first 4 lines are svelte internals; adjust this number if we change the internal call stack
- const stack = get_stack()?.slice(4);
- if (!stack) return null;
-
- for (let i = 0; i < stack.length; i++) {
- const entry = stack[i];
- const modules = boundaries[entry.file];
- if (!modules) {
- // If the first entry is not a component, that means the modification very likely happened
- // within a .svelte.js file, possibly triggered by a component. Since these files are not part
- // of the bondaries/component context heuristic, we need to bail in this case, else we would
- // have false positives when the .svelte.ts file provides a state creator function, encapsulating
- // the state and its mutations, and is being called from a component other than the one who
- // called the state creator function.
- if (i === 0) return null;
- continue;
- }
-
- for (const module of modules) {
- if (module.end == null) {
- return null;
- }
- if (module.start.line < entry.line && module.end.line > entry.line) {
- return module.component;
+export function create_ownership_validator(props) {
+ const component = component_context?.function;
+ const parent = component_context?.p?.function;
+
+ return {
+ /**
+ * @param {string} prop
+ * @param {any[]} path
+ * @param {any} result
+ * @param {number} line
+ * @param {number} column
+ */
+ mutation: (prop, path, result, line, column) => {
+ const name = path[0];
+ if (is_bound_or_unset(props, name) || !parent) {
+ return result;
}
- }
- }
-
- return null;
-}
-export const ADD_OWNER = Symbol('ADD_OWNER');
-
-/**
- * Together with `mark_module_end`, this function establishes the boundaries of a `.svelte` file,
- * such that subsequent calls to `get_component` can tell us which component is responsible
- * for a given state change
- */
-export function mark_module_start() {
- const start = get_stack()?.[2];
-
- if (start) {
- (boundaries[start.file] ??= []).push({
- start,
- // @ts-expect-error
- end: null,
- // @ts-expect-error we add the component at the end, since HMR will overwrite the function
- component: null
- });
- }
-}
+ /** @type {any} */
+ let value = props;
-/**
- * @param {Function} component
- */
-export function mark_module_end(component) {
- const end = get_stack()?.[2];
-
- if (end) {
- const boundaries_file = boundaries[end.file];
- const boundary = boundaries_file[boundaries_file.length - 1];
-
- boundary.end = end;
- boundary.component = component;
- }
-}
-
-/**
- * @param {any} object
- * @param {any | null} owner
- * @param {boolean} [global]
- * @param {boolean} [skip_warning]
- */
-export function add_owner(object, owner, global = false, skip_warning = false) {
- if (object && !global) {
- const component = dev_current_component_function;
- const metadata = object[STATE_SYMBOL_METADATA];
- if (metadata && !has_owner(metadata, component)) {
- let original = get_owner(metadata);
-
- if (owner && owner[FILENAME] !== component[FILENAME] && !skip_warning) {
- w.ownership_invalid_binding(component[FILENAME], owner[FILENAME], original[FILENAME]);
- }
- }
- }
-
- add_owner_to_object(object, owner, new Set());
-}
-
-/**
- * @param {() => unknown} get_object
- * @param {any} Component
- * @param {boolean} [skip_warning]
- */
-export function add_owner_effect(get_object, Component, skip_warning = false) {
- user_pre_effect(() => {
- add_owner(get_object(), Component, false, skip_warning);
- });
-}
-
-/**
- * @param {any} _this
- * @param {Function} owner
- * @param {Array<() => any>} getters
- * @param {boolean} skip_warning
- */
-export function add_owner_to_class(_this, owner, getters, skip_warning) {
- _this[ADD_OWNER].current ||= getters.map(() => UNINITIALIZED);
-
- for (let i = 0; i < getters.length; i += 1) {
- const current = getters[i]();
- // For performance reasons we only re-add the owner if the state has changed
- if (current !== _this[ADD_OWNER][i]) {
- _this[ADD_OWNER].current[i] = current;
- add_owner(current, owner, false, skip_warning);
- }
- }
-}
-
-/**
- * @param {ProxyMetadata | null} from
- * @param {ProxyMetadata} to
- */
-export function widen_ownership(from, to) {
- if (to.owners === null) {
- return;
- }
-
- while (from) {
- if (from.owners === null) {
- to.owners = null;
- break;
- }
-
- for (const owner of from.owners) {
- to.owners.add(owner);
- }
-
- from = from.parent;
- }
-}
-
-/**
- * @param {any} object
- * @param {Function | null} owner If `null`, then the object is globally owned and will not be checked
- * @param {Set} seen
- */
-function add_owner_to_object(object, owner, seen) {
- const metadata = /** @type {ProxyMetadata} */ (object?.[STATE_SYMBOL_METADATA]);
-
- if (metadata) {
- // this is a state proxy, add owner directly, if not globally shared
- if ('owners' in metadata && metadata.owners != null) {
- if (owner) {
- metadata.owners.add(owner);
- } else {
- metadata.owners = null;
+ for (let i = 0; i < path.length - 1; i++) {
+ value = value[path[i]];
+ if (!value?.[STATE_SYMBOL]) {
+ return result;
+ }
}
- }
- } else if (object && typeof object === 'object') {
- if (seen.has(object)) return;
- seen.add(object);
- if (ADD_OWNER in object && object[ADD_OWNER]) {
- // this is a class with state fields. we put this in a render effect
- // so that if state is replaced (e.g. `instance.name = { first, last }`)
- // the new state is also co-owned by the caller of `getContext`
- render_effect(() => {
- object[ADD_OWNER](owner);
- });
- } else {
- var proto = get_prototype_of(object);
- if (proto === Object.prototype) {
- // recurse until we find a state proxy
- for (const key in object) {
- if (Object.getOwnPropertyDescriptor(object, key)?.get) {
- // Similar to the class case; the getter could update with a new state
- let current = UNINITIALIZED;
- render_effect(() => {
- const next = object[key];
- if (current !== next) {
- current = next;
- add_owner_to_object(next, owner, seen);
- }
- });
- } else {
- add_owner_to_object(object[key], owner, seen);
- }
- }
- } else if (proto === Array.prototype) {
- // recurse until we find a state proxy
- for (let i = 0; i < object.length; i += 1) {
- add_owner_to_object(object[i], owner, seen);
- }
+ const location = sanitize_location(`${component[FILENAME]}:${line}:${column}`);
+
+ w.ownership_invalid_mutation(name, location, prop, parent[FILENAME]);
+
+ return result;
+ },
+ /**
+ * @param {any} key
+ * @param {any} child_component
+ * @param {() => any} value
+ */
+ binding: (key, child_component, value) => {
+ if (!is_bound_or_unset(props, key) && parent && value()?.[STATE_SYMBOL]) {
+ w.ownership_invalid_binding(
+ component[FILENAME],
+ key,
+ child_component[FILENAME],
+ parent[FILENAME]
+ );
}
}
- }
+ };
}
/**
- * @param {ProxyMetadata} metadata
- * @param {Function} component
- * @returns {boolean}
+ * @param {Record} props
+ * @param {string} prop_name
*/
-function has_owner(metadata, component) {
- if (metadata.owners === null) {
- return true;
- }
-
- return (
- metadata.owners.has(component) ||
- // This helps avoid false positives when using HMR, where the component function is replaced
- (FILENAME in component &&
- [...metadata.owners].some(
- (owner) => /** @type {any} */ (owner)[FILENAME] === component[FILENAME]
- )) ||
- (metadata.parent !== null && has_owner(metadata.parent, component))
- );
-}
-
-/**
- * @param {ProxyMetadata} metadata
- * @returns {any}
- */
-function get_owner(metadata) {
+function is_bound_or_unset(props, prop_name) {
+ // Can be the case when someone does `mount(Component, props)` with `let props = $state({...})`
+ // or `createClassComponent(Component, props)`
+ const is_entry_props = STATE_SYMBOL in props || LEGACY_PROPS in props;
return (
- metadata?.owners?.values().next().value ??
- get_owner(/** @type {ProxyMetadata} */ (metadata.parent))
+ !!get_descriptor(props, prop_name)?.set ||
+ (is_entry_props && prop_name in props) ||
+ !(prop_name in props)
);
}
-
-let skip = false;
-
-/**
- * @param {() => any} fn
- */
-export function skip_ownership_validation(fn) {
- skip = true;
- fn();
- skip = false;
-}
-
-/**
- * @param {ProxyMetadata} metadata
- */
-export function check_ownership(metadata) {
- if (skip) return;
-
- const component = get_component();
-
- if (component && !has_owner(metadata, component)) {
- let original = get_owner(metadata);
-
- // @ts-expect-error
- if (original[FILENAME] !== component[FILENAME]) {
- // @ts-expect-error
- w.ownership_invalid_mutation(component[FILENAME], original[FILENAME]);
- } else {
- w.ownership_invalid_mutation();
- }
- }
-}
diff --git a/packages/svelte/src/internal/client/dev/tracing.js b/packages/svelte/src/internal/client/dev/tracing.js
index 3881ef3442e7..5834f5bffd0e 100644
--- a/packages/svelte/src/internal/client/dev/tracing.js
+++ b/packages/svelte/src/internal/client/dev/tracing.js
@@ -1,53 +1,43 @@
-/** @import { Derived, Reaction, Signal, Value } from '#client' */
+/** @import { Derived, Reaction, Value } from '#client' */
import { UNINITIALIZED } from '../../../constants.js';
import { snapshot } from '../../shared/clone.js';
import { define_property } from '../../shared/utils.js';
-import { DERIVED, STATE_SYMBOL } from '../constants.js';
+import { DERIVED, PROXY_PATH_SYMBOL, STATE_SYMBOL } from '#client/constants';
import { effect_tracking } from '../reactivity/effects.js';
import { active_reaction, captured_signals, set_captured_signals, untrack } from '../runtime.js';
-/** @type { any } */
+/**
+ * @typedef {{
+ * traces: Error[];
+ * }} TraceEntry
+ */
+
+/** @type {{ reaction: Reaction | null, entries: Map } | null} */
export let tracing_expressions = null;
/**
- * @param { Value } signal
- * @param { { read: Error[] } } [entry]
+ * @param {Value} signal
+ * @param {TraceEntry} [entry]
*/
function log_entry(signal, entry) {
- const debug = signal.debug;
- const value = signal.trace_need_increase ? signal.trace_v : signal.v;
+ const value = signal.v;
if (value === UNINITIALIZED) {
return;
}
- if (debug) {
- var previous_captured_signals = captured_signals;
- var captured = new Set();
- set_captured_signals(captured);
- try {
- untrack(() => {
- debug();
- });
- } finally {
- set_captured_signals(previous_captured_signals);
- }
- if (captured.size > 0) {
- for (const dep of captured) {
- log_entry(dep);
- }
- return;
- }
- }
-
const type = (signal.f & DERIVED) !== 0 ? '$derived' : '$state';
const current_reaction = /** @type {Reaction} */ (active_reaction);
const dirty = signal.wv > current_reaction.wv || current_reaction.wv === 0;
+ const style = dirty
+ ? 'color: CornflowerBlue; font-weight: bold'
+ : 'color: grey; font-weight: normal';
// eslint-disable-next-line no-console
console.groupCollapsed(
- `%c${type}`,
- dirty ? 'color: CornflowerBlue; font-weight: bold' : 'color: grey; font-weight: bold',
+ signal.label ? `%c${type}%c ${signal.label}` : `%c${type}%c`,
+ style,
+ dirty ? 'font-weight: normal' : style,
typeof value === 'object' && value !== null && STATE_SYMBOL in value
? snapshot(value, true)
: value
@@ -65,17 +55,15 @@ function log_entry(signal, entry) {
console.log(signal.created);
}
- if (signal.updated) {
+ if (dirty && signal.updated) {
// eslint-disable-next-line no-console
console.log(signal.updated);
}
- const read = entry?.read;
-
- if (read && read.length > 0) {
- for (var stack of read) {
+ if (entry) {
+ for (var trace of entry.traces) {
// eslint-disable-next-line no-console
- console.log(stack);
+ console.log(trace);
}
}
@@ -90,6 +78,7 @@ function log_entry(signal, entry) {
*/
export function trace(label, fn) {
var previously_tracing_expressions = tracing_expressions;
+
try {
tracing_expressions = { entries: new Map(), reaction: active_reaction };
@@ -97,39 +86,32 @@ export function trace(label, fn) {
var value = fn();
var time = (performance.now() - start).toFixed(2);
+ var prefix = untrack(label);
+
if (!effect_tracking()) {
// eslint-disable-next-line no-console
- console.log(`${label()} %cran outside of an effect (${time}ms)`, 'color: grey');
+ console.log(`${prefix} %cran outside of an effect (${time}ms)`, 'color: grey');
} else if (tracing_expressions.entries.size === 0) {
// eslint-disable-next-line no-console
- console.log(`${label()} %cno reactive dependencies (${time}ms)`, 'color: grey');
+ console.log(`${prefix} %cno reactive dependencies (${time}ms)`, 'color: grey');
} else {
// eslint-disable-next-line no-console
- console.group(`${label()} %c(${time}ms)`, 'color: grey');
+ console.group(`${prefix} %c(${time}ms)`, 'color: grey');
var entries = tracing_expressions.entries;
+ untrack(() => {
+ for (const [signal, traces] of entries) {
+ log_entry(signal, traces);
+ }
+ });
+
tracing_expressions = null;
- for (const [signal, entry] of entries) {
- log_entry(signal, entry);
- }
// eslint-disable-next-line no-console
console.groupEnd();
}
- if (previously_tracing_expressions !== null && tracing_expressions !== null) {
- for (const [signal, entry] of tracing_expressions.entries) {
- var prev_entry = previously_tracing_expressions.get(signal);
-
- if (prev_entry === undefined) {
- previously_tracing_expressions.set(signal, entry);
- } else {
- prev_entry.read.push(...entry.read);
- }
- }
- }
-
return value;
} finally {
tracing_expressions = previously_tracing_expressions;
@@ -177,3 +159,34 @@ export function get_stack(label) {
}
return error;
}
+
+/**
+ * @param {Value} source
+ * @param {string} label
+ */
+export function tag(source, label) {
+ source.label = label;
+ tag_proxy(source.v, label);
+
+ return source;
+}
+
+/**
+ * @param {unknown} value
+ * @param {string} label
+ */
+export function tag_proxy(value, label) {
+ // @ts-expect-error
+ value?.[PROXY_PATH_SYMBOL]?.(label);
+ return value;
+}
+
+/**
+ * @param {unknown} value
+ */
+export function label(value) {
+ if (typeof value === 'symbol') return `Symbol(${value.description})`;
+ if (typeof value === 'function') return '';
+ if (typeof value === 'object' && value) return '';
+ return String(value);
+}
diff --git a/packages/svelte/src/internal/client/dev/validation.js b/packages/svelte/src/internal/client/dev/validation.js
new file mode 100644
index 000000000000..e41e4c46283d
--- /dev/null
+++ b/packages/svelte/src/internal/client/dev/validation.js
@@ -0,0 +1,15 @@
+import { invalid_snippet_arguments } from '../../shared/errors.js';
+/**
+ * @param {Node} anchor
+ * @param {...(()=>any)[]} args
+ */
+export function validate_snippet_args(anchor, ...args) {
+ if (typeof anchor !== 'object' || !(anchor instanceof Node)) {
+ invalid_snippet_arguments();
+ }
+ for (let arg of args) {
+ if (typeof arg !== 'function') {
+ invalid_snippet_arguments();
+ }
+ }
+}
diff --git a/packages/svelte/src/internal/client/dom/blocks/await.js b/packages/svelte/src/internal/client/dom/blocks/await.js
index 2e3d22977914..325224fff237 100644
--- a/packages/svelte/src/internal/client/dom/blocks/await.js
+++ b/packages/svelte/src/internal/client/dom/blocks/await.js
@@ -4,14 +4,23 @@ import { is_promise } from '../../../shared/utils.js';
import { block, branch, pause_effect, resume_effect } from '../../reactivity/effects.js';
import { internal_set, mutable_source, source } from '../../reactivity/sources.js';
import { flushSync, set_active_effect, set_active_reaction } from '../../runtime.js';
-import { hydrate_next, hydrate_node, hydrating } from '../hydration.js';
+import {
+ hydrate_next,
+ hydrate_node,
+ hydrating,
+ remove_nodes,
+ set_hydrate_node,
+ set_hydrating
+} from '../hydration.js';
import { queue_micro_task } from '../task.js';
-import { UNINITIALIZED } from '../../../../constants.js';
+import { HYDRATION_START_ELSE, UNINITIALIZED } from '../../../../constants.js';
import {
component_context,
+ dev_stack,
is_runes,
set_component_context,
- set_dev_current_component_function
+ set_dev_current_component_function,
+ set_dev_stack
} from '../../context.js';
const PENDING = 0;
@@ -38,6 +47,7 @@ export function await_block(node, get_input, pending_fn, then_fn, catch_fn) {
/** @type {any} */
var component_function = DEV ? component_context?.function : null;
+ var dev_original_stack = DEV ? dev_stack : null;
/** @type {V | Promise | typeof UNINITIALIZED} */
var input = UNINITIALIZED;
@@ -51,8 +61,10 @@ export function await_block(node, get_input, pending_fn, then_fn, catch_fn) {
/** @type {Effect | null} */
var catch_effect;
- var input_source = (runes ? source : mutable_source)(/** @type {V} */ (undefined));
- var error_source = (runes ? source : mutable_source)(undefined);
+ var input_source = runes
+ ? source(/** @type {V} */ (undefined))
+ : mutable_source(/** @type {V} */ (undefined), false, false);
+ var error_source = runes ? source(undefined) : mutable_source(undefined, false, false);
var resolved = false;
/**
@@ -66,7 +78,10 @@ export function await_block(node, get_input, pending_fn, then_fn, catch_fn) {
set_active_effect(effect);
set_active_reaction(effect); // TODO do we need both?
set_component_context(active_component_context);
- if (DEV) set_dev_current_component_function(component_function);
+ if (DEV) {
+ set_dev_current_component_function(component_function);
+ set_dev_stack(dev_original_stack);
+ }
}
try {
@@ -98,7 +113,11 @@ export function await_block(node, get_input, pending_fn, then_fn, catch_fn) {
}
} finally {
if (restore) {
- if (DEV) set_dev_current_component_function(null);
+ if (DEV) {
+ set_dev_current_component_function(null);
+ set_dev_stack(null);
+ }
+
set_component_context(null);
set_active_reaction(null);
set_active_effect(null);
@@ -113,6 +132,19 @@ export function await_block(node, get_input, pending_fn, then_fn, catch_fn) {
var effect = block(() => {
if (input === (input = get_input())) return;
+ /** Whether or not there was a hydration mismatch. Needs to be a `let` or else it isn't treeshaken out */
+ // @ts-ignore coercing `anchor` to a `Comment` causes TypeScript and Prettier to fight
+ let mismatch = hydrating && is_promise(input) === (anchor.data === HYDRATION_START_ELSE);
+
+ if (mismatch) {
+ // Hydration mismatch: remove everything inside the anchor and start fresh
+ anchor = remove_nodes();
+
+ set_hydrate_node(anchor);
+ set_hydrating(false);
+ mismatch = true;
+ }
+
if (is_promise(input)) {
var promise = input;
@@ -155,6 +187,11 @@ export function await_block(node, get_input, pending_fn, then_fn, catch_fn) {
update(THEN, false);
}
+ if (mismatch) {
+ // continue in hydration mode
+ set_hydrating(true);
+ }
+
// Set the input to something else, in order to disable the promise callbacks
return () => (input = UNINITIALIZED);
});
diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js
index c1ca7a960034..03cec4fd2fcd 100644
--- a/packages/svelte/src/internal/client/dom/blocks/boundary.js
+++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js
@@ -1,15 +1,13 @@
/** @import { Effect, TemplateNode, } from '#client' */
-
-import { BOUNDARY_EFFECT, EFFECT_TRANSPARENT } from '../../constants.js';
+import { BOUNDARY_EFFECT, EFFECT_PRESERVED, EFFECT_TRANSPARENT } from '#client/constants';
import { component_context, set_component_context } from '../../context.js';
+import { invoke_error_boundary } from '../../error-handling.js';
import { block, branch, destroy_effect, pause_effect } from '../../reactivity/effects.js';
import {
active_effect,
active_reaction,
- handle_error,
set_active_effect,
- set_active_reaction,
- reset_is_throwing_error
+ set_active_reaction
} from '../../runtime.js';
import {
hydrate_next,
@@ -22,112 +20,170 @@ import {
import { queue_micro_task } from '../task.js';
/**
- * @param {Effect} boundary
- * @param {() => void} fn
+ * @typedef {{
+ * onerror?: (error: unknown, reset: () => void) => void;
+ * failed?: (anchor: Node, error: () => unknown, reset: () => () => void) => void;
+ * }} BoundaryProps
*/
-function with_boundary(boundary, fn) {
- var previous_effect = active_effect;
- var previous_reaction = active_reaction;
- var previous_ctx = component_context;
-
- set_active_effect(boundary);
- set_active_reaction(boundary);
- set_component_context(boundary.ctx);
-
- try {
- fn();
- } finally {
- set_active_effect(previous_effect);
- set_active_reaction(previous_reaction);
- set_component_context(previous_ctx);
- }
-}
+
+var flags = EFFECT_TRANSPARENT | EFFECT_PRESERVED | BOUNDARY_EFFECT;
/**
* @param {TemplateNode} node
- * @param {{
- * onerror?: (error: unknown, reset: () => void) => void,
- * failed?: (anchor: Node, error: () => unknown, reset: () => () => void) => void
- * }} props
- * @param {((anchor: Node) => void)} boundary_fn
+ * @param {BoundaryProps} props
+ * @param {((anchor: Node) => void)} children
* @returns {void}
*/
-export function boundary(node, props, boundary_fn) {
- var anchor = node;
+export function boundary(node, props, children) {
+ new Boundary(node, props, children);
+}
+
+export class Boundary {
+ /** @type {TemplateNode} */
+ #anchor;
+
+ /** @type {TemplateNode} */
+ #hydrate_open;
+
+ /** @type {BoundaryProps} */
+ #props;
+
+ /** @type {((anchor: Node) => void)} */
+ #children;
/** @type {Effect} */
- var boundary_effect;
-
- block(() => {
- var boundary = /** @type {Effect} */ (active_effect);
- var hydrate_open = hydrate_node;
- var is_creating_fallback = false;
-
- // We re-use the effect's fn property to avoid allocation of an additional field
- boundary.fn = (/** @type {unknown}} */ error) => {
- var onerror = props.onerror;
- let failed = props.failed;
-
- // If we have nothing to capture the error, or if we hit an error while
- // rendering the fallback, re-throw for another boundary to handle
- if ((!onerror && !failed) || is_creating_fallback) {
- throw error;
- }
+ #effect;
- var reset = () => {
- pause_effect(boundary_effect);
+ /** @type {Effect | null} */
+ #main_effect = null;
- with_boundary(boundary, () => {
- is_creating_fallback = false;
- boundary_effect = branch(() => boundary_fn(anchor));
- reset_is_throwing_error();
- });
- };
+ /** @type {Effect | null} */
+ #failed_effect = null;
- onerror?.(error, reset);
+ #is_creating_fallback = false;
+
+ /**
+ * @param {TemplateNode} node
+ * @param {BoundaryProps} props
+ * @param {((anchor: Node) => void)} children
+ */
+ constructor(node, props, children) {
+ this.#anchor = node;
+ this.#props = props;
+ this.#children = children;
+
+ this.#hydrate_open = hydrate_node;
+
+ this.#effect = block(() => {
+ /** @type {Effect} */ (active_effect).b = this;
- if (boundary_effect) {
- destroy_effect(boundary_effect);
- } else if (hydrating) {
- set_hydrate_node(hydrate_open);
- next();
- set_hydrate_node(remove_nodes());
+ if (hydrating) {
+ hydrate_next();
}
- if (failed) {
- // Render the `failed` snippet in a microtask
- queue_micro_task(() => {
- with_boundary(boundary, () => {
- is_creating_fallback = true;
-
- try {
- boundary_effect = branch(() => {
- failed(
- anchor,
- () => error,
- () => reset
- );
- });
- } catch (error) {
- handle_error(error, boundary, null, boundary.ctx);
- }
-
- reset_is_throwing_error();
- is_creating_fallback = false;
- });
+ try {
+ this.#main_effect = branch(() => children(this.#anchor));
+ } catch (error) {
+ this.error(error);
+ }
+ }, flags);
+
+ if (hydrating) {
+ this.#anchor = hydrate_node;
+ }
+ }
+
+ /**
+ * @param {() => Effect | null} fn
+ */
+ #run(fn) {
+ var previous_effect = active_effect;
+ var previous_reaction = active_reaction;
+ var previous_ctx = component_context;
+
+ set_active_effect(this.#effect);
+ set_active_reaction(this.#effect);
+ set_component_context(this.#effect.ctx);
+
+ try {
+ return fn();
+ } finally {
+ set_active_effect(previous_effect);
+ set_active_reaction(previous_reaction);
+ set_component_context(previous_ctx);
+ }
+ }
+
+ /** @param {unknown} error */
+ error(error) {
+ var onerror = this.#props.onerror;
+ let failed = this.#props.failed;
+
+ const reset = () => {
+ if (this.#failed_effect !== null) {
+ pause_effect(this.#failed_effect, () => {
+ this.#failed_effect = null;
});
}
+
+ this.#main_effect = this.#run(() => {
+ this.#is_creating_fallback = false;
+ return branch(() => this.#children(this.#anchor));
+ });
};
- if (hydrating) {
- hydrate_next();
+ // If we have nothing to capture the error, or if we hit an error while
+ // rendering the fallback, re-throw for another boundary to handle
+ if (this.#is_creating_fallback || (!onerror && !failed)) {
+ throw error;
}
- boundary_effect = branch(() => boundary_fn(anchor));
- reset_is_throwing_error();
- }, EFFECT_TRANSPARENT | BOUNDARY_EFFECT);
+ var previous_reaction = active_reaction;
+
+ try {
+ set_active_reaction(null);
+ onerror?.(error, reset);
+ } finally {
+ set_active_reaction(previous_reaction);
+ }
- if (hydrating) {
- anchor = hydrate_node;
+ if (this.#main_effect) {
+ destroy_effect(this.#main_effect);
+ this.#main_effect = null;
+ }
+
+ if (this.#failed_effect) {
+ destroy_effect(this.#failed_effect);
+ this.#failed_effect = null;
+ }
+
+ if (hydrating) {
+ set_hydrate_node(this.#hydrate_open);
+ next();
+ set_hydrate_node(remove_nodes());
+ }
+
+ if (failed) {
+ queue_micro_task(() => {
+ this.#failed_effect = this.#run(() => {
+ this.#is_creating_fallback = true;
+
+ try {
+ return branch(() => {
+ failed(
+ this.#anchor,
+ () => error,
+ () => reset
+ );
+ });
+ } catch (error) {
+ invoke_error_boundary(error, /** @type {Effect} */ (this.#effect.parent));
+ return null;
+ } finally {
+ this.#is_creating_fallback = false;
+ }
+ });
+ });
+ }
}
}
diff --git a/packages/svelte/src/internal/client/dom/blocks/css-props.js b/packages/svelte/src/internal/client/dom/blocks/css-props.js
index 473d35b122ad..ef361987539f 100644
--- a/packages/svelte/src/internal/client/dom/blocks/css-props.js
+++ b/packages/svelte/src/internal/client/dom/blocks/css-props.js
@@ -1,6 +1,6 @@
/** @import { TemplateNode } from '#client' */
import { render_effect, teardown } from '../../reactivity/effects.js';
-import { hydrate_node, hydrating, set_hydrate_node } from '../hydration.js';
+import { hydrating, set_hydrate_node } from '../hydration.js';
import { get_first_child } from '../operations.js';
/**
@@ -26,8 +26,4 @@ export function css_props(element, get_styles) {
}
}
});
-
- teardown(() => {
- element.remove();
- });
}
diff --git a/packages/svelte/src/internal/client/dom/blocks/each.js b/packages/svelte/src/internal/client/dom/blocks/each.js
index 3baa03a91753..7b12be58e836 100644
--- a/packages/svelte/src/internal/client/dom/blocks/each.js
+++ b/packages/svelte/src/internal/client/dom/blocks/each.js
@@ -12,6 +12,7 @@ import {
hydrate_next,
hydrate_node,
hydrating,
+ read_hydration_instruction,
remove_nodes,
set_hydrate_node,
set_hydrating
@@ -33,9 +34,9 @@ import {
} from '../../reactivity/effects.js';
import { source, mutable_source, internal_set } from '../../reactivity/sources.js';
import { array_from, is_array } from '../../../shared/utils.js';
-import { INERT } from '../../constants.js';
+import { COMMENT_NODE, INERT } from '#client/constants';
import { queue_micro_task } from '../task.js';
-import { active_effect, active_reaction, get } from '../../runtime.js';
+import { active_effect, get } from '../../runtime.js';
import { DEV } from 'esm-env';
import { derived_safe_equal } from '../../reactivity/deriveds.js';
@@ -160,7 +161,7 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f
let mismatch = false;
if (hydrating) {
- var is_else = /** @type {Comment} */ (anchor).data === HYDRATION_START_ELSE;
+ var is_else = read_hydration_instruction(anchor) === HYDRATION_START_ELSE;
if (is_else !== (length === 0)) {
// hydration mismatch — remove the server-rendered DOM and start over
@@ -182,7 +183,7 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f
for (var i = 0; i < length; i++) {
if (
- hydrate_node.nodeType === 8 &&
+ hydrate_node.nodeType === COMMENT_NODE &&
/** @type {Comment} */ (hydrate_node).data === HYDRATION_END
) {
// The server rendered fewer items than expected,
@@ -520,13 +521,13 @@ function create_item(
var reactive = (flags & EACH_ITEM_REACTIVE) !== 0;
var mutable = (flags & EACH_ITEM_IMMUTABLE) === 0;
- var v = reactive ? (mutable ? mutable_source(value) : source(value)) : value;
+ var v = reactive ? (mutable ? mutable_source(value, false, false) : source(value)) : value;
var i = (flags & EACH_INDEX_REACTIVE) === 0 ? index : source(index);
if (DEV && reactive) {
// For tracing purposes, we need to link the source signal we create with the
// collection + index so that tracing works as intended
- /** @type {Value} */ (v).debug = () => {
+ /** @type {Value} */ (v).trace = () => {
var collection_index = typeof i === 'number' ? index : i.v;
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
get_collection()[collection_index];
diff --git a/packages/svelte/src/internal/client/dom/blocks/html.js b/packages/svelte/src/internal/client/dom/blocks/html.js
index b3fc5a9c725d..d7190abc6668 100644
--- a/packages/svelte/src/internal/client/dom/blocks/html.js
+++ b/packages/svelte/src/internal/client/dom/blocks/html.js
@@ -1,6 +1,6 @@
/** @import { Effect, TemplateNode } from '#client' */
import { FILENAME, HYDRATION_ERROR } from '../../../../constants.js';
-import { block, branch, destroy_effect } from '../../reactivity/effects.js';
+import { remove_effect_dom, template_effect } from '../../reactivity/effects.js';
import { hydrate_next, hydrate_node, hydrating, set_hydrate_node } from '../hydration.js';
import { create_fragment_from_html } from '../reconciler.js';
import { assign_nodes } from '../template.js';
@@ -9,6 +9,8 @@ import { hash, sanitize_location } from '../../../../utils.js';
import { DEV } from 'esm-env';
import { dev_current_component_function } from '../../context.js';
import { get_first_child, get_next_sibling } from '../operations.js';
+import { active_effect } from '../../runtime.js';
+import { COMMENT_NODE } from '#client/constants';
/**
* @param {Element} element
@@ -34,89 +36,84 @@ function check_hash(element, server_hash, value) {
/**
* @param {Element | Text | Comment} node
* @param {() => string} get_value
- * @param {boolean} svg
- * @param {boolean} mathml
+ * @param {boolean} [svg]
+ * @param {boolean} [mathml]
* @param {boolean} [skip_warning]
* @returns {void}
*/
-export function html(node, get_value, svg, mathml, skip_warning) {
+export function html(node, get_value, svg = false, mathml = false, skip_warning = false) {
var anchor = node;
var value = '';
- /** @type {Effect | undefined} */
- var effect;
+ template_effect(() => {
+ var effect = /** @type {Effect} */ (active_effect);
- block(() => {
if (value === (value = get_value() ?? '')) {
- if (hydrating) {
- hydrate_next();
- }
+ if (hydrating) hydrate_next();
return;
}
- if (effect !== undefined) {
- destroy_effect(effect);
- effect = undefined;
+ if (effect.nodes_start !== null) {
+ remove_effect_dom(effect.nodes_start, /** @type {TemplateNode} */ (effect.nodes_end));
+ effect.nodes_start = effect.nodes_end = null;
}
if (value === '') return;
- effect = branch(() => {
- if (hydrating) {
- // We're deliberately not trying to repair mismatches between server and client,
- // as it's costly and error-prone (and it's an edge case to have a mismatch anyway)
- var hash = /** @type {Comment} */ (hydrate_node).data;
- var next = hydrate_next();
- var last = next;
-
- while (
- next !== null &&
- (next.nodeType !== 8 || /** @type {Comment} */ (next).data !== '')
- ) {
- last = next;
- next = /** @type {TemplateNode} */ (get_next_sibling(next));
- }
-
- if (next === null) {
- w.hydration_mismatch();
- throw HYDRATION_ERROR;
- }
-
- if (DEV && !skip_warning) {
- check_hash(/** @type {Element} */ (next.parentNode), hash, value);
- }
-
- assign_nodes(hydrate_node, last);
- anchor = set_hydrate_node(next);
- return;
+ if (hydrating) {
+ // We're deliberately not trying to repair mismatches between server and client,
+ // as it's costly and error-prone (and it's an edge case to have a mismatch anyway)
+ var hash = /** @type {Comment} */ (hydrate_node).data;
+ var next = hydrate_next();
+ var last = next;
+
+ while (
+ next !== null &&
+ (next.nodeType !== COMMENT_NODE || /** @type {Comment} */ (next).data !== '')
+ ) {
+ last = next;
+ next = /** @type {TemplateNode} */ (get_next_sibling(next));
}
- var html = value + '';
- if (svg) html = `${html} `;
- else if (mathml) html = `${html} `;
-
- // Don't use create_fragment_with_script_from_html here because that would mean script tags are executed.
- // @html is basically `.innerHTML = ...` and that doesn't execute scripts either due to security reasons.
- /** @type {DocumentFragment | Element} */
- var node = create_fragment_from_html(html);
+ if (next === null) {
+ w.hydration_mismatch();
+ throw HYDRATION_ERROR;
+ }
- if (svg || mathml) {
- node = /** @type {Element} */ (get_first_child(node));
+ if (DEV && !skip_warning) {
+ check_hash(/** @type {Element} */ (next.parentNode), hash, value);
}
- assign_nodes(
- /** @type {TemplateNode} */ (get_first_child(node)),
- /** @type {TemplateNode} */ (node.lastChild)
- );
-
- if (svg || mathml) {
- while (get_first_child(node)) {
- anchor.before(/** @type {Node} */ (get_first_child(node)));
- }
- } else {
- anchor.before(node);
+ assign_nodes(hydrate_node, last);
+ anchor = set_hydrate_node(next);
+ return;
+ }
+
+ var html = value + '';
+ if (svg) html = `${html} `;
+ else if (mathml) html = `${html} `;
+
+ // Don't use create_fragment_with_script_from_html here because that would mean script tags are executed.
+ // @html is basically `.innerHTML = ...` and that doesn't execute scripts either due to security reasons.
+ /** @type {DocumentFragment | Element} */
+ var node = create_fragment_from_html(html);
+
+ if (svg || mathml) {
+ node = /** @type {Element} */ (get_first_child(node));
+ }
+
+ assign_nodes(
+ /** @type {TemplateNode} */ (get_first_child(node)),
+ /** @type {TemplateNode} */ (node.lastChild)
+ );
+
+ if (svg || mathml) {
+ while (get_first_child(node)) {
+ anchor.before(/** @type {Node} */ (get_first_child(node)));
}
- });
+ } else {
+ anchor.before(node);
+ }
});
}
diff --git a/packages/svelte/src/internal/client/dom/blocks/if.js b/packages/svelte/src/internal/client/dom/blocks/if.js
index 423c436fe4ef..bf1098c3f465 100644
--- a/packages/svelte/src/internal/client/dom/blocks/if.js
+++ b/packages/svelte/src/internal/client/dom/blocks/if.js
@@ -1,9 +1,10 @@
/** @import { Effect, TemplateNode } from '#client' */
-import { EFFECT_TRANSPARENT } from '../../constants.js';
+import { EFFECT_TRANSPARENT } from '#client/constants';
import {
hydrate_next,
hydrate_node,
hydrating,
+ read_hydration_instruction,
remove_nodes,
set_hydrate_node,
set_hydrating
@@ -56,7 +57,8 @@ export function if_block(node, fn, [root_index, hydrate_index] = [0, 0]) {
if (hydrating && hydrate_index !== -1) {
if (root_index === 0) {
- const data = /** @type {Comment} */ (anchor).data;
+ const data = read_hydration_instruction(anchor);
+
if (data === HYDRATION_START) {
hydrate_index = 0;
} else if (data === HYDRATION_START_ELSE) {
diff --git a/packages/svelte/src/internal/client/dom/blocks/snippet.js b/packages/svelte/src/internal/client/dom/blocks/snippet.js
index b916a02ce55f..32d88d4c606a 100644
--- a/packages/svelte/src/internal/client/dom/blocks/snippet.js
+++ b/packages/svelte/src/internal/client/dom/blocks/snippet.js
@@ -1,7 +1,7 @@
/** @import { Snippet } from 'svelte' */
/** @import { Effect, TemplateNode } from '#client' */
/** @import { Getters } from '#shared' */
-import { EFFECT_TRANSPARENT } from '../../constants.js';
+import { EFFECT_TRANSPARENT, ELEMENT_NODE } from '#client/constants';
import { branch, block, destroy_effect, teardown } from '../../reactivity/effects.js';
import {
dev_current_component_function,
@@ -15,6 +15,7 @@ import * as e from '../../errors.js';
import { DEV } from 'esm-env';
import { get_first_child, get_next_sibling } from '../operations.js';
import { noop } from '../../../shared/utils.js';
+import { prevent_snippet_stringification } from '../../../shared/validate.js';
/**
* @template {(node: TemplateNode, ...args: any[]) => void} SnippetFn
@@ -60,7 +61,7 @@ export function snippet(node, get_snippet, ...args) {
* @param {(node: TemplateNode, ...args: any[]) => void} fn
*/
export function wrap_snippet(component, fn) {
- return (/** @type {TemplateNode} */ node, /** @type {any[]} */ ...args) => {
+ const snippet = (/** @type {TemplateNode} */ node, /** @type {any[]} */ ...args) => {
var previous_component_function = dev_current_component_function;
set_dev_current_component_function(component);
@@ -70,6 +71,10 @@ export function wrap_snippet(component, fn) {
set_dev_current_component_function(previous_component_function);
}
};
+
+ prevent_snippet_stringification(snippet);
+
+ return snippet;
}
/**
@@ -97,7 +102,7 @@ export function createRawSnippet(fn) {
var fragment = create_fragment_from_html(html);
element = /** @type {Element} */ (get_first_child(fragment));
- if (DEV && (get_next_sibling(element) !== null || element.nodeType !== 1)) {
+ if (DEV && (get_next_sibling(element) !== null || element.nodeType !== ELEMENT_NODE)) {
w.invalid_raw_snippet_render();
}
diff --git a/packages/svelte/src/internal/client/dom/blocks/svelte-component.js b/packages/svelte/src/internal/client/dom/blocks/svelte-component.js
index 72157eaa40db..ad21436505d0 100644
--- a/packages/svelte/src/internal/client/dom/blocks/svelte-component.js
+++ b/packages/svelte/src/internal/client/dom/blocks/svelte-component.js
@@ -1,5 +1,5 @@
/** @import { TemplateNode, Dom, Effect } from '#client' */
-import { EFFECT_TRANSPARENT } from '../../constants.js';
+import { EFFECT_TRANSPARENT } from '#client/constants';
import { block, branch, pause_effect } from '../../reactivity/effects.js';
import { hydrate_next, hydrate_node, hydrating } from '../hydration.js';
diff --git a/packages/svelte/src/internal/client/dom/blocks/svelte-element.js b/packages/svelte/src/internal/client/dom/blocks/svelte-element.js
index 18641300e537..231a3621b1af 100644
--- a/packages/svelte/src/internal/client/dom/blocks/svelte-element.js
+++ b/packages/svelte/src/internal/client/dom/blocks/svelte-element.js
@@ -18,9 +18,9 @@ import {
import { set_should_intro } from '../../render.js';
import { current_each_item, set_current_each_item } from './each.js';
import { active_effect } from '../../runtime.js';
-import { component_context } from '../../context.js';
+import { component_context, dev_stack } from '../../context.js';
import { DEV } from 'esm-env';
-import { EFFECT_TRANSPARENT } from '../../constants.js';
+import { EFFECT_TRANSPARENT, ELEMENT_NODE } from '#client/constants';
import { assign_nodes } from '../template.js';
import { is_raw_text_element } from '../../../../utils.js';
@@ -51,7 +51,7 @@ export function element(node, get_tag, is_svg, render_fn, get_namespace, locatio
/** @type {null | Element} */
var element = null;
- if (hydrating && hydrate_node.nodeType === 1) {
+ if (hydrating && hydrate_node.nodeType === ELEMENT_NODE) {
element = /** @type {Element} */ (hydrate_node);
hydrate_next();
}
@@ -107,6 +107,7 @@ export function element(node, get_tag, is_svg, render_fn, get_namespace, locatio
if (DEV && location) {
// @ts-expect-error
element.__svelte_meta = {
+ parent: dev_stack,
loc: {
file: filename,
line: location[0],
diff --git a/packages/svelte/src/internal/client/dom/blocks/svelte-head.js b/packages/svelte/src/internal/client/dom/blocks/svelte-head.js
index e3e3eacad7c7..66d337183637 100644
--- a/packages/svelte/src/internal/client/dom/blocks/svelte-head.js
+++ b/packages/svelte/src/internal/client/dom/blocks/svelte-head.js
@@ -2,7 +2,7 @@
import { hydrate_node, hydrating, set_hydrate_node, set_hydrating } from '../hydration.js';
import { create_text, get_first_child, get_next_sibling } from '../operations.js';
import { block } from '../../reactivity/effects.js';
-import { HEAD_EFFECT } from '../../constants.js';
+import { COMMENT_NODE, HEAD_EFFECT } from '#client/constants';
import { HYDRATION_START } from '../../../../constants.js';
/**
@@ -37,7 +37,8 @@ export function head(render_fn) {
while (
head_anchor !== null &&
- (head_anchor.nodeType !== 8 || /** @type {Comment} */ (head_anchor).data !== HYDRATION_START)
+ (head_anchor.nodeType !== COMMENT_NODE ||
+ /** @type {Comment} */ (head_anchor).data !== HYDRATION_START)
) {
head_anchor = /** @type {TemplateNode} */ (get_next_sibling(head_anchor));
}
diff --git a/packages/svelte/src/internal/client/dom/elements/attachments.js b/packages/svelte/src/internal/client/dom/elements/attachments.js
new file mode 100644
index 000000000000..4fc128013888
--- /dev/null
+++ b/packages/svelte/src/internal/client/dom/elements/attachments.js
@@ -0,0 +1,33 @@
+/** @import { Effect } from '#client' */
+import { block, branch, effect, destroy_effect } from '../../reactivity/effects.js';
+
+// TODO in 6.0 or 7.0, when we remove legacy mode, we can simplify this by
+// getting rid of the block/branch stuff and just letting the effect rip.
+// see https://github.com/sveltejs/svelte/pull/15962
+
+/**
+ * @param {Element} node
+ * @param {() => (node: Element) => void} get_fn
+ */
+export function attach(node, get_fn) {
+ /** @type {false | undefined | ((node: Element) => void)} */
+ var fn = undefined;
+
+ /** @type {Effect | null} */
+ var e;
+
+ block(() => {
+ if (fn !== (fn = get_fn())) {
+ if (e) {
+ destroy_effect(e);
+ e = null;
+ }
+
+ if (fn) {
+ e = branch(() => {
+ effect(() => /** @type {(node: Element) => void} */ (fn)(node));
+ });
+ }
+ }
+ });
+}
diff --git a/packages/svelte/src/internal/client/dom/elements/attributes.js b/packages/svelte/src/internal/client/dom/elements/attributes.js
index dd408dcf8715..5db685cf3e90 100644
--- a/packages/svelte/src/internal/client/dom/elements/attributes.js
+++ b/packages/svelte/src/internal/client/dom/elements/attributes.js
@@ -1,24 +1,35 @@
+/** @import { Effect } from '#client' */
import { DEV } from 'esm-env';
import { hydrating, set_hydrating } from '../hydration.js';
import { get_descriptors, get_prototype_of } from '../../../shared/utils.js';
import { create_event, delegate } from './events.js';
import { add_form_reset_listener, autofocus } from './misc.js';
import * as w from '../../warnings.js';
-import { LOADING_ATTR_SYMBOL } from '../../constants.js';
+import { LOADING_ATTR_SYMBOL } from '#client/constants';
import { queue_idle_task } from '../task.js';
import { is_capture_event, is_delegated, normalize_attribute } from '../../../../utils.js';
import {
active_effect,
active_reaction,
+ get,
set_active_effect,
set_active_reaction
} from '../../runtime.js';
+import { attach } from './attachments.js';
import { clsx } from '../../../shared/attributes.js';
import { set_class } from './class.js';
+import { set_style } from './style.js';
+import { ATTACHMENT_KEY, NAMESPACE_HTML } from '../../../../constants.js';
+import { block, branch, destroy_effect, effect } from '../../reactivity/effects.js';
+import { derived } from '../../reactivity/deriveds.js';
+import { init_select, select_option } from './bindings/select.js';
export const CLASS = Symbol('class');
export const STYLE = Symbol('style');
+const IS_CUSTOM_ELEMENT = Symbol('is custom element');
+const IS_HTML = Symbol('is html');
+
/**
* The value/checked attribute in the template actually corresponds to the defaultValue property, so we need
* to remove it upon hydration to avoid a bug when someone resets the form value.
@@ -63,8 +74,7 @@ export function remove_input_defaults(input) {
* @param {any} value
*/
export function set_value(element, value) {
- // @ts-expect-error
- var attributes = (element.__attributes ??= {});
+ var attributes = get_attributes(element);
if (
attributes.value ===
@@ -87,8 +97,7 @@ export function set_value(element, value) {
* @param {boolean} checked
*/
export function set_checked(element, checked) {
- // @ts-expect-error
- var attributes = (element.__attributes ??= {});
+ var attributes = get_attributes(element);
if (
attributes.checked ===
@@ -151,8 +160,7 @@ export function set_default_value(element, value) {
* @param {boolean} [skip_warning]
*/
export function set_attribute(element, attribute, value, skip_warning) {
- // @ts-expect-error
- var attributes = (element.__attributes ??= {});
+ var attributes = get_attributes(element);
if (hydrating) {
attributes[attribute] = element.getAttribute(attribute);
@@ -176,11 +184,6 @@ export function set_attribute(element, attribute, value, skip_warning) {
if (attributes[attribute] === (attributes[attribute] = value)) return;
- if (attribute === 'style' && '__styles' in element) {
- // reset styles to force style: directive to update
- element.__styles = {};
- }
-
if (attribute === 'loading') {
// @ts-expect-error
element[LOADING_ATTR_SYMBOL] = value;
@@ -217,6 +220,7 @@ export function set_custom_element_data(node, prop, value) {
// or effect
var previous_reaction = active_reaction;
var previous_effect = active_effect;
+
// If we're hydrating but the custom element is from Svelte, and it already scaffolded,
// then it might run block logic in hydration mode, which we have to prevent.
let was_hydrating = hydrating;
@@ -226,17 +230,20 @@ export function set_custom_element_data(node, prop, value) {
set_active_reaction(null);
set_active_effect(null);
+
try {
if (
+ // `style` should use `set_attribute` rather than the setter
+ prop !== 'style' &&
// Don't compute setters for custom elements while they aren't registered yet,
// because during their upgrade/instantiation they might add more setters.
// Instead, fall back to a simple "an object, then set as property" heuristic.
- setters_cache.has(node.nodeName) ||
+ (setters_cache.has(node.nodeName) ||
// customElements may not be available in browser extension contexts
!customElements ||
customElements.get(node.tagName.toLowerCase())
? get_setters(node).includes(prop)
- : value && typeof value === 'object'
+ : value && typeof value === 'object')
) {
// @ts-expect-error
node[prop] = value;
@@ -261,20 +268,15 @@ export function set_custom_element_data(node, prop, value) {
* @param {Record | undefined} prev
* @param {Record} next New attributes - this function mutates this object
* @param {string} [css_hash]
- * @param {boolean} [preserve_attribute_case]
- * @param {boolean} [is_custom_element]
* @param {boolean} [skip_warning]
* @returns {Record}
*/
-export function set_attributes(
- element,
- prev,
- next,
- css_hash,
- preserve_attribute_case = false,
- is_custom_element = false,
- skip_warning = false
-) {
+export function set_attributes(element, prev, next, css_hash, skip_warning = false) {
+ var attributes = get_attributes(element);
+
+ var is_custom_element = attributes[IS_CUSTOM_ELEMENT];
+ var preserve_attribute_case = !attributes[IS_HTML];
+
// If we're hydrating but the custom element is from Svelte, and it already scaffolded,
// then it might run block logic in hydration mode, which we have to prevent.
let is_hydrating_custom_element = hydrating && is_custom_element;
@@ -297,10 +299,11 @@ export function set_attributes(
next.class = null; /* force call to set_class() */
}
- var setters = get_setters(element);
+ if (next[STYLE]) {
+ next.style ??= null; /* force call to set_style() */
+ }
- // @ts-expect-error
- var attributes = /** @type {Record} **/ (element.__attributes ??= {});
+ var setters = get_setters(element);
// since key is captured we use const
for (const key in next) {
@@ -334,8 +337,19 @@ export function set_attributes(
continue;
}
+ if (key === 'style') {
+ set_style(element, value, prev?.[STYLE], next[STYLE]);
+ current[key] = value;
+ current[STYLE] = next[STYLE];
+ continue;
+ }
+
var prev_value = current[key];
- if (value === prev_value) continue;
+
+ // Skip if value is unchanged, unless it's `undefined` and the element still has the attribute
+ if (value === prev_value && !(value === undefined && element.hasAttribute(key))) {
+ continue;
+ }
current[key] = value;
@@ -385,8 +399,9 @@ export function set_attributes(
// @ts-ignore
element[`__${event_name}`] = undefined;
}
- } else if (key === 'style' && value != null) {
- element.style.cssText = value + '';
+ } else if (key === 'style') {
+ // avoid using the setter
+ set_attribute(element, key, value);
} else if (key === 'autofocus') {
autofocus(/** @type {HTMLElement} */ (element), Boolean(value));
} else if (!is_custom_element && (key === '__value' || (key === 'value' && value != null))) {
@@ -432,13 +447,9 @@ export function set_attributes(
// @ts-ignore
element[name] = value;
} else if (typeof value !== 'function') {
- set_attribute(element, name, value);
+ set_attribute(element, name, value, skip_warning);
}
}
- if (key === 'style' && '__styles' in element) {
- // reset styles to force style: directive to update
- element.__styles = {};
- }
}
if (is_hydrating_custom_element) {
@@ -448,6 +459,85 @@ export function set_attributes(
return current;
}
+/**
+ * @param {Element & ElementCSSInlineStyle} element
+ * @param {(...expressions: any) => Record} fn
+ * @param {Array<() => any>} thunks
+ * @param {string} [css_hash]
+ * @param {boolean} [skip_warning]
+ */
+export function attribute_effect(
+ element,
+ fn,
+ thunks = [],
+ css_hash,
+ skip_warning = false,
+ d = derived
+) {
+ const deriveds = thunks.map(d);
+
+ /** @type {Record | undefined} */
+ var prev = undefined;
+
+ /** @type {Record} */
+ var effects = {};
+
+ var is_select = element.nodeName === 'SELECT';
+ var inited = false;
+
+ block(() => {
+ var next = fn(...deriveds.map(get));
+ /** @type {Record} */
+ var current = set_attributes(element, prev, next, css_hash, skip_warning);
+
+ if (inited && is_select && 'value' in next) {
+ select_option(/** @type {HTMLSelectElement} */ (element), next.value, false);
+ }
+
+ for (let symbol of Object.getOwnPropertySymbols(effects)) {
+ if (!next[symbol]) destroy_effect(effects[symbol]);
+ }
+
+ for (let symbol of Object.getOwnPropertySymbols(next)) {
+ var n = next[symbol];
+
+ if (symbol.description === ATTACHMENT_KEY && (!prev || n !== prev[symbol])) {
+ if (effects[symbol]) destroy_effect(effects[symbol]);
+ effects[symbol] = branch(() => attach(element, () => n));
+ }
+
+ current[symbol] = n;
+ }
+
+ prev = current;
+ });
+
+ if (is_select) {
+ var select = /** @type {HTMLSelectElement} */ (element);
+
+ effect(() => {
+ select_option(select, /** @type {Record} */ (prev).value);
+ init_select(select);
+ });
+ }
+
+ inited = true;
+}
+
+/**
+ *
+ * @param {Element} element
+ */
+function get_attributes(element) {
+ return /** @type {Record} **/ (
+ // @ts-expect-error
+ element.__attributes ??= {
+ [IS_CUSTOM_ELEMENT]: element.nodeName.includes('-'),
+ [IS_HTML]: element.namespaceURI === NAMESPACE_HTML
+ }
+ );
+}
+
/** @type {Map} */
var setters_cache = new Map();
diff --git a/packages/svelte/src/internal/client/dom/elements/bindings/media.js b/packages/svelte/src/internal/client/dom/elements/bindings/media.js
index 4893426d5552..f96ee0598969 100644
--- a/packages/svelte/src/internal/client/dom/elements/bindings/media.js
+++ b/packages/svelte/src/internal/client/dom/elements/bindings/media.js
@@ -1,4 +1,3 @@
-import { hydrating } from '../../hydration.js';
import { render_effect, effect, teardown } from '../../../reactivity/effects.js';
import { listen } from './shared.js';
@@ -63,7 +62,23 @@ export function bind_current_time(media, get, set = get) {
* @param {(array: Array<{ start: number; end: number }>) => void} set
*/
export function bind_buffered(media, set) {
- listen(media, ['loadedmetadata', 'progress'], () => set(time_ranges_to_array(media.buffered)));
+ /** @type {{ start: number; end: number; }[]} */
+ var current;
+
+ // `buffered` can update without emitting any event, so we check it on various events.
+ // By specs, `buffered` always returns a new object, so we have to compare deeply.
+ listen(media, ['loadedmetadata', 'progress', 'timeupdate', 'seeking'], () => {
+ var ranges = media.buffered;
+
+ if (
+ !current ||
+ current.length !== ranges.length ||
+ current.some((range, i) => ranges.start(i) !== range.start || ranges.end(i) !== range.end)
+ ) {
+ current = time_ranges_to_array(ranges);
+ set(current);
+ }
+ });
}
/**
diff --git a/packages/svelte/src/internal/client/dom/elements/bindings/select.js b/packages/svelte/src/internal/client/dom/elements/bindings/select.js
index 32cd160de21e..5e89686d8654 100644
--- a/packages/svelte/src/internal/client/dom/elements/bindings/select.js
+++ b/packages/svelte/src/internal/client/dom/elements/bindings/select.js
@@ -1,7 +1,8 @@
-import { effect } from '../../../reactivity/effects.js';
+import { effect, teardown } from '../../../reactivity/effects.js';
import { listen_to_event_and_reset_event } from './shared.js';
-import { untrack } from '../../../runtime.js';
import { is } from '../../../proxy.js';
+import { is_array } from '../../../../shared/utils.js';
+import * as w from '../../../warnings.js';
/**
* Selects the correct option(s) (depending on whether this is a multiple select)
@@ -12,10 +13,25 @@ import { is } from '../../../proxy.js';
*/
export function select_option(select, value, mounting) {
if (select.multiple) {
- return select_options(select, value);
+ // If value is null or undefined, keep the selection as is
+ if (value == undefined) {
+ return;
+ }
+
+ // If not an array, warn and keep the selection as is
+ if (!is_array(value)) {
+ return w.select_multiple_invalid_value();
+ }
+
+ // Otherwise, update the selection
+ for (var option of select.options) {
+ option.selected = value.includes(get_option_value(option));
+ }
+
+ return;
}
- for (var option of select.options) {
+ for (option of select.options) {
var option_value = get_option_value(option);
if (is(option_value, value)) {
option.selected = true;
@@ -34,40 +50,29 @@ export function select_option(select, value, mounting) {
* current selection to the dom when it changes. Such
* changes could for example occur when options are
* inside an `#each` block.
- * @template V
* @param {HTMLSelectElement} select
- * @param {() => V} [get_value]
*/
-export function init_select(select, get_value) {
- let mounting = true;
- effect(() => {
- if (get_value) {
- select_option(select, untrack(get_value), mounting);
- }
- mounting = false;
+export function init_select(select) {
+ var observer = new MutationObserver(() => {
+ // @ts-ignore
+ select_option(select, select.__value);
+ // Deliberately don't update the potential binding value,
+ // the model should be preserved unless explicitly changed
+ });
+
+ observer.observe(select, {
+ // Listen to option element changes
+ childList: true,
+ subtree: true, // because of
+ // Listen to option element value attribute changes
+ // (doesn't get notified of select value changes,
+ // because that property is not reflected as an attribute)
+ attributes: true,
+ attributeFilter: ['value']
+ });
- var observer = new MutationObserver(() => {
- // @ts-ignore
- var value = select.__value;
- select_option(select, value);
- // Deliberately don't update the potential binding value,
- // the model should be preserved unless explicitly changed
- });
-
- observer.observe(select, {
- // Listen to option element changes
- childList: true,
- subtree: true, // because of
- // Listen to option element value attribute changes
- // (doesn't get notified of select value changes,
- // because that property is not reflected as an attribute)
- attributes: true,
- attributeFilter: ['value']
- });
-
- return () => {
- observer.disconnect();
- };
+ teardown(() => {
+ observer.disconnect();
});
}
@@ -119,22 +124,9 @@ export function bind_select_value(select, get, set = get) {
mounting = false;
});
- // don't pass get_value, we already initialize it in the effect above
init_select(select);
}
-/**
- * @template V
- * @param {HTMLSelectElement} select
- * @param {V} value
- */
-function select_options(select, value) {
- for (var option of select.options) {
- // @ts-ignore
- option.selected = ~value.indexOf(get_option_value(option));
- }
-}
-
/** @param {HTMLOptionElement} option */
function get_option_value(option) {
// __value only exists if the has a value attribute
diff --git a/packages/svelte/src/internal/client/dom/elements/bindings/this.js b/packages/svelte/src/internal/client/dom/elements/bindings/this.js
index 56b0a56e71c4..e9bbcedc6f69 100644
--- a/packages/svelte/src/internal/client/dom/elements/bindings/this.js
+++ b/packages/svelte/src/internal/client/dom/elements/bindings/this.js
@@ -1,4 +1,4 @@
-import { STATE_SYMBOL } from '../../../constants.js';
+import { STATE_SYMBOL } from '#client/constants';
import { effect, render_effect } from '../../../reactivity/effects.js';
import { untrack } from '../../../runtime.js';
import { queue_micro_task } from '../../task.js';
diff --git a/packages/svelte/src/internal/client/dom/elements/class.js b/packages/svelte/src/internal/client/dom/elements/class.js
index 7027c84f6260..038ce33f3eee 100644
--- a/packages/svelte/src/internal/client/dom/elements/class.js
+++ b/packages/svelte/src/internal/client/dom/elements/class.js
@@ -14,13 +14,17 @@ export function set_class(dom, is_html, value, hash, prev_classes, next_classes)
// @ts-expect-error need to add __className to patched prototype
var prev = dom.__className;
- if (hydrating || prev !== value) {
+ if (
+ hydrating ||
+ prev !== value ||
+ prev === undefined // for edge case of `class={undefined}`
+ ) {
var next_class_name = to_class(value, hash, next_classes);
if (!hydrating || next_class_name !== dom.getAttribute('class')) {
// Removing the attribute when the value is only an empty string causes
// performance issues vs simply making the className an empty string. So
- // we should only remove the class if the the value is nullish
+ // we should only remove the class if the value is nullish
// and there no hash/directives :
if (next_class_name == null) {
dom.removeAttribute('class');
@@ -33,7 +37,7 @@ export function set_class(dom, is_html, value, hash, prev_classes, next_classes)
// @ts-expect-error need to add __className to patched prototype
dom.__className = value;
- } else if (next_classes) {
+ } else if (next_classes && prev_classes !== next_classes) {
for (var key in next_classes) {
var is_present = !!next_classes[key];
diff --git a/packages/svelte/src/internal/client/dom/elements/custom-element.js b/packages/svelte/src/internal/client/dom/elements/custom-element.js
index 6195b2c561d5..2d118bfab3a4 100644
--- a/packages/svelte/src/internal/client/dom/elements/custom-element.js
+++ b/packages/svelte/src/internal/client/dom/elements/custom-element.js
@@ -1,5 +1,5 @@
import { createClassComponent } from '../../../../legacy/legacy-client.js';
-import { destroy_effect, effect_root, render_effect } from '../../reactivity/effects.js';
+import { effect_root, render_effect } from '../../reactivity/effects.js';
import { append } from '../template.js';
import { define_property, get_descriptor, object_keys } from '../../../shared/utils.js';
diff --git a/packages/svelte/src/internal/client/dom/elements/events.js b/packages/svelte/src/internal/client/dom/elements/events.js
index 25ece5f569d7..fa3bf0b0210a 100644
--- a/packages/svelte/src/internal/client/dom/elements/events.js
+++ b/packages/svelte/src/internal/client/dom/elements/events.js
@@ -1,4 +1,3 @@
-/** @import { Location } from 'locate-character' */
import { teardown } from '../../reactivity/effects.js';
import { define_property, is_array } from '../../../shared/utils.js';
import { hydrating } from '../hydration.js';
@@ -27,12 +26,8 @@ export const root_event_handles = new Set();
export function replay_events(dom) {
if (!hydrating) return;
- if (dom.onload) {
- dom.removeAttribute('onload');
- }
- if (dom.onerror) {
- dom.removeAttribute('onerror');
- }
+ dom.removeAttribute('onload');
+ dom.removeAttribute('onerror');
// @ts-expect-error
const event = dom.__e;
if (event !== undefined) {
@@ -117,8 +112,15 @@ export function event(event_name, dom, handler, capture, passive) {
var options = { capture, passive };
var target_handler = create_event(event_name, dom, handler, options);
- // @ts-ignore
- if (dom === document.body || dom === window || dom === document) {
+ if (
+ dom === document.body ||
+ // @ts-ignore
+ dom === window ||
+ // @ts-ignore
+ dom === document ||
+ // Firefox has quirky behavior, it can happen that we still get "canplay" events when the element is already removed
+ dom instanceof HTMLMediaElement
+ ) {
teardown(() => {
dom.removeEventListener(event_name, target_handler, options);
});
@@ -238,7 +240,7 @@ export function handle_event_propagation(event) {
var delegated = current_target['__' + event_name];
if (
- delegated !== undefined &&
+ delegated != null &&
(!(/** @type {any} */ (current_target).disabled) ||
// DOM could've been updated already by the time this is reached, so we check this as well
// -> the target could not have been disabled because it emits the event in the first place
@@ -311,13 +313,11 @@ export function apply(
error = e;
}
- if (typeof handler === 'function') {
- handler.apply(element, args);
- } else if (has_side_effects || handler != null || error) {
+ if (typeof handler !== 'function' && (has_side_effects || handler != null || error)) {
const filename = component?.[FILENAME];
const location = loc ? ` at ${filename}:${loc[0]}:${loc[1]}` : ` in ${filename}`;
-
- const event_name = args[0].type;
+ const phase = args[0]?.eventPhase < Event.BUBBLING_PHASE ? 'capture' : '';
+ const event_name = args[0]?.type + phase;
const description = `\`${event_name}\` handler${location}`;
const suggestion = remove_parens ? 'remove the trailing `()`' : 'add a leading `() =>`';
@@ -327,4 +327,5 @@ export function apply(
throw error;
}
}
+ handler?.apply(element, args);
}
diff --git a/packages/svelte/src/internal/client/dom/elements/style.js b/packages/svelte/src/internal/client/dom/elements/style.js
index 34531029c9c6..3e05eec30efa 100644
--- a/packages/svelte/src/internal/client/dom/elements/style.js
+++ b/packages/svelte/src/internal/client/dom/elements/style.js
@@ -1,22 +1,57 @@
+import { to_style } from '../../../shared/attributes.js';
+import { hydrating } from '../hydration.js';
+
/**
- * @param {HTMLElement} dom
- * @param {string} key
- * @param {string} value
- * @param {boolean} [important]
+ * @param {Element & ElementCSSInlineStyle} dom
+ * @param {Record} prev
+ * @param {Record} next
+ * @param {string} [priority]
*/
-export function set_style(dom, key, value, important) {
- // @ts-expect-error
- var styles = (dom.__styles ??= {});
+function update_styles(dom, prev = {}, next, priority) {
+ for (var key in next) {
+ var value = next[key];
- if (styles[key] === value) {
- return;
+ if (prev[key] !== value) {
+ if (next[key] == null) {
+ dom.style.removeProperty(key);
+ } else {
+ dom.style.setProperty(key, value, priority);
+ }
+ }
}
+}
- styles[key] = value;
+/**
+ * @param {Element & ElementCSSInlineStyle} dom
+ * @param {string | null} value
+ * @param {Record | [Record, Record]} [prev_styles]
+ * @param {Record | [Record, Record]} [next_styles]
+ */
+export function set_style(dom, value, prev_styles, next_styles) {
+ // @ts-expect-error
+ var prev = dom.__style;
+
+ if (hydrating || prev !== value) {
+ var next_style_attr = to_style(value, next_styles);
- if (value == null) {
- dom.style.removeProperty(key);
- } else {
- dom.style.setProperty(key, value, important ? 'important' : '');
+ if (!hydrating || next_style_attr !== dom.getAttribute('style')) {
+ if (next_style_attr == null) {
+ dom.removeAttribute('style');
+ } else {
+ dom.style.cssText = next_style_attr;
+ }
+ }
+
+ // @ts-expect-error
+ dom.__style = value;
+ } else if (next_styles) {
+ if (Array.isArray(next_styles)) {
+ update_styles(dom, prev_styles?.[0], next_styles[0]);
+ update_styles(dom, prev_styles?.[1], next_styles[1], 'important');
+ } else {
+ update_styles(dom, prev_styles, next_styles);
+ }
}
+
+ return next_styles;
}
diff --git a/packages/svelte/src/internal/client/dom/elements/transitions.js b/packages/svelte/src/internal/client/dom/elements/transitions.js
index fbc1da95df95..38100e982cce 100644
--- a/packages/svelte/src/internal/client/dom/elements/transitions.js
+++ b/packages/svelte/src/internal/client/dom/elements/transitions.js
@@ -12,7 +12,7 @@ import { loop } from '../../loop.js';
import { should_intro } from '../../render.js';
import { current_each_item } from '../blocks/each.js';
import { TRANSITION_GLOBAL, TRANSITION_IN, TRANSITION_OUT } from '../../../../constants.js';
-import { BLOCK_EFFECT, EFFECT_RAN, EFFECT_TRANSPARENT } from '../../constants.js';
+import { BLOCK_EFFECT, EFFECT_RAN, EFFECT_TRANSPARENT } from '#client/constants';
import { queue_micro_task } from '../task.js';
import { without_reactive_context } from './bindings/shared.js';
@@ -381,9 +381,15 @@ function animate(element, options, counterpart, t2, on_finish) {
// create a dummy animation that lasts as long as the delay (but with whatever devtools
// multiplier is in effect). in the common case that it is `0`, we keep it anyway so that
// the CSS keyframes aren't created until the DOM is updated
- var animation = element.animate(keyframes, { duration: delay });
+ //
+ // fill forwards to prevent the element from rendering without styles applied
+ // see https://github.com/sveltejs/svelte/issues/14732
+ var animation = element.animate(keyframes, { duration: delay, fill: 'forwards' });
animation.onfinish = () => {
+ // remove dummy animation from the stack to prevent conflict with main animation
+ animation.cancel();
+
// for bidirectional transitions, we start from the current position,
// rather than doing a full intro/outro
var t1 = counterpart?.t() ?? 1 - t2;
diff --git a/packages/svelte/src/internal/client/dom/hydration.js b/packages/svelte/src/internal/client/dom/hydration.js
index 8523ff97d559..1f80b7922bc2 100644
--- a/packages/svelte/src/internal/client/dom/hydration.js
+++ b/packages/svelte/src/internal/client/dom/hydration.js
@@ -1,5 +1,6 @@
/** @import { TemplateNode } from '#client' */
+import { COMMENT_NODE } from '#client/constants';
import {
HYDRATION_END,
HYDRATION_ERROR,
@@ -87,7 +88,7 @@ export function remove_nodes() {
var node = hydrate_node;
while (true) {
- if (node.nodeType === 8) {
+ if (node.nodeType === COMMENT_NODE) {
var data = /** @type {Comment} */ (node).data;
if (data === HYDRATION_END) {
@@ -103,3 +104,16 @@ export function remove_nodes() {
node = next;
}
}
+
+/**
+ *
+ * @param {TemplateNode} node
+ */
+export function read_hydration_instruction(node) {
+ if (!node || node.nodeType !== COMMENT_NODE) {
+ w.hydration_mismatch();
+ throw HYDRATION_ERROR;
+ }
+
+ return /** @type {Comment} */ (node).data;
+}
diff --git a/packages/svelte/src/internal/client/dom/legacy/event-modifiers.js b/packages/svelte/src/internal/client/dom/legacy/event-modifiers.js
index 918832dfa532..2e5312f1b054 100644
--- a/packages/svelte/src/internal/client/dom/legacy/event-modifiers.js
+++ b/packages/svelte/src/internal/client/dom/legacy/event-modifiers.js
@@ -1,4 +1,3 @@
-/** @import { ActionReturn } from 'svelte/action' */
import { noop } from '../../../shared/utils.js';
import { user_pre_effect } from '../../reactivity/effects.js';
import { on } from '../elements/events.js';
diff --git a/packages/svelte/src/internal/client/dom/operations.js b/packages/svelte/src/internal/client/dom/operations.js
index f6ac92456e78..4b35f0802f4a 100644
--- a/packages/svelte/src/internal/client/dom/operations.js
+++ b/packages/svelte/src/internal/client/dom/operations.js
@@ -2,7 +2,8 @@
import { hydrate_node, hydrating, set_hydrate_node } from './hydration.js';
import { DEV } from 'esm-env';
import { init_array_prototype_warnings } from '../dev/equality.js';
-import { get_descriptor } from '../../shared/utils.js';
+import { get_descriptor, is_extensible } from '../../shared/utils.js';
+import { TEXT_NODE } from '#client/constants';
// export these for reference in the compiled code, making global name deduplication unnecessary
/** @type {Window} */
@@ -34,26 +35,31 @@ export function init_operations() {
var element_prototype = Element.prototype;
var node_prototype = Node.prototype;
+ var text_prototype = Text.prototype;
// @ts-ignore
first_child_getter = get_descriptor(node_prototype, 'firstChild').get;
// @ts-ignore
next_sibling_getter = get_descriptor(node_prototype, 'nextSibling').get;
- // the following assignments improve perf of lookups on DOM nodes
- // @ts-expect-error
- element_prototype.__click = undefined;
- // @ts-expect-error
- element_prototype.__className = undefined;
- // @ts-expect-error
- element_prototype.__attributes = null;
- // @ts-expect-error
- element_prototype.__styles = null;
- // @ts-expect-error
- element_prototype.__e = undefined;
-
- // @ts-expect-error
- Text.prototype.__t = undefined;
+ if (is_extensible(element_prototype)) {
+ // the following assignments improve perf of lookups on DOM nodes
+ // @ts-expect-error
+ element_prototype.__click = undefined;
+ // @ts-expect-error
+ element_prototype.__className = undefined;
+ // @ts-expect-error
+ element_prototype.__attributes = null;
+ // @ts-expect-error
+ element_prototype.__style = undefined;
+ // @ts-expect-error
+ element_prototype.__e = undefined;
+ }
+
+ if (is_extensible(text_prototype)) {
+ // @ts-expect-error
+ text_prototype.__t = undefined;
+ }
if (DEV) {
// @ts-expect-error
@@ -108,7 +114,7 @@ export function child(node, is_text) {
// Child can be null if we have an element with a single child, like `{text}
`, where `text` is empty
if (child === null) {
child = hydrate_node.appendChild(create_text());
- } else if (is_text && child.nodeType !== 3) {
+ } else if (is_text && child.nodeType !== TEXT_NODE) {
var text = create_text();
child?.before(text);
set_hydrate_node(text);
@@ -138,7 +144,7 @@ export function first_child(fragment, is_text) {
// if an {expression} is empty during SSR, there might be no
// text node to hydrate — we must therefore create one
- if (is_text && hydrate_node?.nodeType !== 3) {
+ if (is_text && hydrate_node?.nodeType !== TEXT_NODE) {
var text = create_text();
hydrate_node?.before(text);
@@ -169,11 +175,9 @@ export function sibling(node, count = 1, is_text = false) {
return next_sibling;
}
- var type = next_sibling?.nodeType;
-
// if a sibling {expression} is empty during SSR, there might be no
// text node to hydrate — we must therefore create one
- if (is_text && type !== 3) {
+ if (is_text && next_sibling?.nodeType !== TEXT_NODE) {
var text = create_text();
// If the next sibling is `null` and we're handling text then it's because
// the SSR content was empty for the text, so we need to generate a new text
@@ -199,3 +203,44 @@ export function sibling(node, count = 1, is_text = false) {
export function clear_text_content(node) {
node.textContent = '';
}
+
+/**
+ *
+ * @param {string} tag
+ * @param {string} [namespace]
+ * @param {string} [is]
+ * @returns
+ */
+export function create_element(tag, namespace, is) {
+ let options = is ? { is } : undefined;
+ if (namespace) {
+ return document.createElementNS(namespace, tag, options);
+ }
+ return document.createElement(tag, options);
+}
+
+export function create_fragment() {
+ return document.createDocumentFragment();
+}
+
+/**
+ * @param {string} data
+ * @returns
+ */
+export function create_comment(data = '') {
+ return document.createComment(data);
+}
+
+/**
+ * @param {Element} element
+ * @param {string} key
+ * @param {string} value
+ * @returns
+ */
+export function set_attribute(element, key, value = '') {
+ if (key.startsWith('xlink:')) {
+ element.setAttributeNS('http://www.w3.org/1999/xlink', key, value);
+ return;
+ }
+ return element.setAttribute(key, value);
+}
diff --git a/packages/svelte/src/internal/client/dom/reconciler.js b/packages/svelte/src/internal/client/dom/reconciler.js
index 9897e08d5314..8d3b5c04958f 100644
--- a/packages/svelte/src/internal/client/dom/reconciler.js
+++ b/packages/svelte/src/internal/client/dom/reconciler.js
@@ -1,6 +1,6 @@
/** @param {string} html */
export function create_fragment_from_html(html) {
var elem = document.createElement('template');
- elem.innerHTML = html;
+ elem.innerHTML = html.replaceAll('', ''); // XHTML compliance
return elem.content;
}
diff --git a/packages/svelte/src/internal/client/dom/template.js b/packages/svelte/src/internal/client/dom/template.js
index de2df62c927f..ebbf0039b269 100644
--- a/packages/svelte/src/internal/client/dom/template.js
+++ b/packages/svelte/src/internal/client/dom/template.js
@@ -1,9 +1,26 @@
/** @import { Effect, TemplateNode } from '#client' */
+/** @import { TemplateStructure } from './types' */
import { hydrate_next, hydrate_node, hydrating, set_hydrate_node } from './hydration.js';
-import { create_text, get_first_child, is_firefox } from './operations.js';
+import {
+ create_text,
+ get_first_child,
+ is_firefox,
+ create_element,
+ create_fragment,
+ create_comment,
+ set_attribute
+} from './operations.js';
import { create_fragment_from_html } from './reconciler.js';
import { active_effect } from '../runtime.js';
-import { TEMPLATE_FRAGMENT, TEMPLATE_USE_IMPORT_NODE } from '../../../constants.js';
+import {
+ NAMESPACE_MATHML,
+ NAMESPACE_SVG,
+ TEMPLATE_FRAGMENT,
+ TEMPLATE_USE_IMPORT_NODE,
+ TEMPLATE_USE_MATHML,
+ TEMPLATE_USE_SVG
+} from '../../../constants.js';
+import { COMMENT_NODE, DOCUMENT_FRAGMENT_NODE, TEXT_NODE } from '#client/constants';
/**
* @param {TemplateNode} start
@@ -23,7 +40,7 @@ export function assign_nodes(start, end) {
* @returns {() => Node | Node[]}
*/
/*#__NO_SIDE_EFFECTS__*/
-export function template(content, flags) {
+export function from_html(content, flags) {
var is_fragment = (flags & TEMPLATE_FRAGMENT) !== 0;
var use_import_node = (flags & TEMPLATE_USE_IMPORT_NODE) !== 0;
@@ -64,17 +81,6 @@ export function template(content, flags) {
};
}
-/**
- * @param {string} content
- * @param {number} flags
- * @returns {() => Node | Node[]}
- */
-/*#__NO_SIDE_EFFECTS__*/
-export function template_with_script(content, flags) {
- var fn = template(content, flags);
- return () => run_scripts(/** @type {Element | DocumentFragment} */ (fn()));
-}
-
/**
* @param {string} content
* @param {number} flags
@@ -82,7 +88,7 @@ export function template_with_script(content, flags) {
* @returns {() => Node | Node[]}
*/
/*#__NO_SIDE_EFFECTS__*/
-export function ns_template(content, flags, ns = 'svg') {
+function from_namespace(content, flags, ns = 'svg') {
/**
* Whether or not the first item is a text/element node. If not, we need to
* create an additional comment node to act as `effect.nodes.start`
@@ -133,22 +139,120 @@ export function ns_template(content, flags, ns = 'svg') {
/**
* @param {string} content
* @param {number} flags
- * @returns {() => Node | Node[]}
*/
/*#__NO_SIDE_EFFECTS__*/
-export function svg_template_with_script(content, flags) {
- var fn = ns_template(content, flags);
- return () => run_scripts(/** @type {Element | DocumentFragment} */ (fn()));
+export function from_svg(content, flags) {
+ return from_namespace(content, flags, 'svg');
}
/**
* @param {string} content
* @param {number} flags
+ */
+/*#__NO_SIDE_EFFECTS__*/
+export function from_mathml(content, flags) {
+ return from_namespace(content, flags, 'math');
+}
+
+/**
+ * @param {TemplateStructure[]} structure
+ * @param {NAMESPACE_SVG | NAMESPACE_MATHML | undefined} [ns]
+ */
+function fragment_from_tree(structure, ns) {
+ var fragment = create_fragment();
+
+ for (var item of structure) {
+ if (typeof item === 'string') {
+ fragment.append(create_text(item));
+ continue;
+ }
+
+ // if `preserveComments === true`, comments are represented as `['// ']`
+ if (item === undefined || item[0][0] === '/') {
+ fragment.append(create_comment(item ? item[0].slice(3) : ''));
+ continue;
+ }
+
+ const [name, attributes, ...children] = item;
+
+ const namespace = name === 'svg' ? NAMESPACE_SVG : name === 'math' ? NAMESPACE_MATHML : ns;
+
+ var element = create_element(name, namespace, attributes?.is);
+
+ for (var key in attributes) {
+ set_attribute(element, key, attributes[key]);
+ }
+
+ if (children.length > 0) {
+ var target =
+ element.tagName === 'TEMPLATE'
+ ? /** @type {HTMLTemplateElement} */ (element).content
+ : element;
+
+ target.append(
+ fragment_from_tree(children, element.tagName === 'foreignObject' ? undefined : namespace)
+ );
+ }
+
+ fragment.append(element);
+ }
+
+ return fragment;
+}
+
+/**
+ * @param {TemplateStructure[]} structure
+ * @param {number} flags
* @returns {() => Node | Node[]}
*/
/*#__NO_SIDE_EFFECTS__*/
-export function mathml_template(content, flags) {
- return ns_template(content, flags, 'math');
+export function from_tree(structure, flags) {
+ var is_fragment = (flags & TEMPLATE_FRAGMENT) !== 0;
+ var use_import_node = (flags & TEMPLATE_USE_IMPORT_NODE) !== 0;
+
+ /** @type {Node} */
+ var node;
+
+ return () => {
+ if (hydrating) {
+ assign_nodes(hydrate_node, null);
+ return hydrate_node;
+ }
+
+ if (node === undefined) {
+ const ns =
+ (flags & TEMPLATE_USE_SVG) !== 0
+ ? NAMESPACE_SVG
+ : (flags & TEMPLATE_USE_MATHML) !== 0
+ ? NAMESPACE_MATHML
+ : undefined;
+
+ node = fragment_from_tree(structure, ns);
+ if (!is_fragment) node = /** @type {Node} */ (get_first_child(node));
+ }
+
+ var clone = /** @type {TemplateNode} */ (
+ use_import_node || is_firefox ? document.importNode(node, true) : node.cloneNode(true)
+ );
+
+ if (is_fragment) {
+ var start = /** @type {TemplateNode} */ (get_first_child(clone));
+ var end = /** @type {TemplateNode} */ (clone.lastChild);
+
+ assign_nodes(start, end);
+ } else {
+ assign_nodes(clone, clone);
+ }
+
+ return clone;
+ };
+}
+
+/**
+ * @param {() => Element | DocumentFragment} fn
+ */
+export function with_script(fn) {
+ return () => run_scripts(fn());
}
/**
@@ -161,7 +265,7 @@ function run_scripts(node) {
// scripts were SSR'd, in which case they will run
if (hydrating) return node;
- const is_fragment = node.nodeType === 11;
+ const is_fragment = node.nodeType === DOCUMENT_FRAGMENT_NODE;
const scripts =
/** @type {HTMLElement} */ (node).tagName === 'SCRIPT'
? [/** @type {HTMLScriptElement} */ (node)]
@@ -202,7 +306,7 @@ export function text(value = '') {
var node = hydrate_node;
- if (node.nodeType !== 3) {
+ if (node.nodeType !== TEXT_NODE) {
// if an {expression} is empty during SSR, we need to insert an empty text node
node.before((node = create_text()));
set_hydrate_node(node);
@@ -257,7 +361,7 @@ export function props_id() {
if (
hydrating &&
hydrate_node &&
- hydrate_node.nodeType === 8 &&
+ hydrate_node.nodeType === COMMENT_NODE &&
hydrate_node.textContent?.startsWith(`#`)
) {
const id = hydrate_node.textContent.substring(1);
diff --git a/packages/svelte/src/internal/client/dom/types.d.ts b/packages/svelte/src/internal/client/dom/types.d.ts
new file mode 100644
index 000000000000..bc3525919b97
--- /dev/null
+++ b/packages/svelte/src/internal/client/dom/types.d.ts
@@ -0,0 +1,4 @@
+export type TemplateStructure =
+ | string
+ | undefined
+ | [string, Record | undefined, ...TemplateStructure[]];
diff --git a/packages/svelte/src/internal/client/error-handling.js b/packages/svelte/src/internal/client/error-handling.js
new file mode 100644
index 000000000000..378f7408ef44
--- /dev/null
+++ b/packages/svelte/src/internal/client/error-handling.js
@@ -0,0 +1,94 @@
+/** @import { Effect } from '#client' */
+/** @import { Boundary } from './dom/blocks/boundary.js' */
+import { DEV } from 'esm-env';
+import { FILENAME } from '../../constants.js';
+import { is_firefox } from './dom/operations.js';
+import { BOUNDARY_EFFECT, EFFECT_RAN } from './constants.js';
+import { define_property, get_descriptor } from '../shared/utils.js';
+import { active_effect } from './runtime.js';
+
+/**
+ * @param {unknown} error
+ */
+export function handle_error(error) {
+ var effect = /** @type {Effect} */ (active_effect);
+
+ if (DEV && error instanceof Error) {
+ adjust_error(error, effect);
+ }
+
+ if ((effect.f & EFFECT_RAN) === 0) {
+ // if the error occurred while creating this subtree, we let it
+ // bubble up until it hits a boundary that can handle it
+ if ((effect.f & BOUNDARY_EFFECT) === 0) {
+ throw error;
+ }
+
+ // @ts-expect-error
+ effect.fn(error);
+ } else {
+ // otherwise we bubble up the effect tree ourselves
+ invoke_error_boundary(error, effect);
+ }
+}
+
+/**
+ * @param {unknown} error
+ * @param {Effect | null} effect
+ */
+export function invoke_error_boundary(error, effect) {
+ while (effect !== null) {
+ if ((effect.f & BOUNDARY_EFFECT) !== 0) {
+ try {
+ /** @type {Boundary} */ (effect.b).error(error);
+ return;
+ } catch {}
+ }
+
+ effect = effect.parent;
+ }
+
+ throw error;
+}
+
+/** @type {WeakSet} */
+const adjusted_errors = new WeakSet();
+
+/**
+ * Add useful information to the error message/stack in development
+ * @param {Error} error
+ * @param {Effect} effect
+ */
+function adjust_error(error, effect) {
+ if (adjusted_errors.has(error)) return;
+ adjusted_errors.add(error);
+
+ const message_descriptor = get_descriptor(error, 'message');
+
+ // if the message was already changed and it's not configurable we can't change it
+ // or it will throw a different error swallowing the original error
+ if (message_descriptor && !message_descriptor.configurable) return;
+
+ var indent = is_firefox ? ' ' : '\t';
+ var component_stack = `\n${indent}in ${effect.fn?.name || ''}`;
+ var context = effect.ctx;
+
+ while (context !== null) {
+ component_stack += `\n${indent}in ${context.function?.[FILENAME].split('/').pop()}`;
+ context = context.p;
+ }
+
+ define_property(error, 'message', {
+ value: error.message + `\n${component_stack}\n`
+ });
+
+ if (error.stack) {
+ // Filter out internal modules
+ define_property(error, 'stack', {
+ value: error.stack
+ .split('\n')
+ .filter((line) => !line.includes('svelte/src/internal'))
+ .join('\n')
+ });
+ }
+}
diff --git a/packages/svelte/src/internal/client/errors.js b/packages/svelte/src/internal/client/errors.js
index 682816e1d64b..5c3f5340e1d1 100644
--- a/packages/svelte/src/internal/client/errors.js
+++ b/packages/svelte/src/internal/client/errors.js
@@ -11,6 +11,7 @@ export function bind_invalid_checkbox_value() {
const error = new Error(`bind_invalid_checkbox_value\nUsing \`bind:value\` together with a checkbox input is not allowed. Use \`bind:checked\` instead\nhttps://svelte.dev/e/bind_invalid_checkbox_value`);
error.name = 'Svelte error';
+
throw error;
} else {
throw new Error(`https://svelte.dev/e/bind_invalid_checkbox_value`);
@@ -29,6 +30,7 @@ export function bind_invalid_export(component, key, name) {
const error = new Error(`bind_invalid_export\nComponent ${component} has an export named \`${key}\` that a consumer component is trying to access using \`bind:${key}\`, which is disallowed. Instead, use \`bind:this\` (e.g. \`<${name} bind:this={component} />\`) and then access the property on the bound component instance (e.g. \`component.${key}\`)\nhttps://svelte.dev/e/bind_invalid_export`);
error.name = 'Svelte error';
+
throw error;
} else {
throw new Error(`https://svelte.dev/e/bind_invalid_export`);
@@ -47,6 +49,7 @@ export function bind_not_bindable(key, component, name) {
const error = new Error(`bind_not_bindable\nA component is attempting to bind to a non-bindable property \`${key}\` belonging to ${component} (i.e. \`<${name} bind:${key}={...}>\`). To mark a property as bindable: \`let { ${key} = $bindable() } = $props()\`\nhttps://svelte.dev/e/bind_not_bindable`);
error.name = 'Svelte error';
+
throw error;
} else {
throw new Error(`https://svelte.dev/e/bind_not_bindable`);
@@ -54,17 +57,17 @@ export function bind_not_bindable(key, component, name) {
}
/**
- * %parent% called `%method%` on an instance of %component%, which is no longer valid in Svelte 5
- * @param {string} parent
+ * Calling `%method%` on a component instance (of %component%) is no longer valid in Svelte 5
* @param {string} method
* @param {string} component
* @returns {never}
*/
-export function component_api_changed(parent, method, component) {
+export function component_api_changed(method, component) {
if (DEV) {
- const error = new Error(`component_api_changed\n${parent} called \`${method}\` on an instance of ${component}, which is no longer valid in Svelte 5\nhttps://svelte.dev/e/component_api_changed`);
+ const error = new Error(`component_api_changed\nCalling \`${method}\` on a component instance (of ${component}) is no longer valid in Svelte 5\nhttps://svelte.dev/e/component_api_changed`);
error.name = 'Svelte error';
+
throw error;
} else {
throw new Error(`https://svelte.dev/e/component_api_changed`);
@@ -82,6 +85,7 @@ export function component_api_invalid_new(component, name) {
const error = new Error(`component_api_invalid_new\nAttempted to instantiate ${component} with \`new ${name}\`, which is no longer valid in Svelte 5. If this component is not under your control, set the \`compatibility.componentApi\` compiler option to \`4\` to keep it working.\nhttps://svelte.dev/e/component_api_invalid_new`);
error.name = 'Svelte error';
+
throw error;
} else {
throw new Error(`https://svelte.dev/e/component_api_invalid_new`);
@@ -97,6 +101,7 @@ export function derived_references_self() {
const error = new Error(`derived_references_self\nA derived value cannot reference itself recursively\nhttps://svelte.dev/e/derived_references_self`);
error.name = 'Svelte error';
+
throw error;
} else {
throw new Error(`https://svelte.dev/e/derived_references_self`);
@@ -112,9 +117,12 @@ export function derived_references_self() {
*/
export function each_key_duplicate(a, b, value) {
if (DEV) {
- const error = new Error(`each_key_duplicate\n${value ? `Keyed each block has duplicate key \`${value}\` at indexes ${a} and ${b}` : `Keyed each block has duplicate key at indexes ${a} and ${b}`}\nhttps://svelte.dev/e/each_key_duplicate`);
+ const error = new Error(`each_key_duplicate\n${value
+ ? `Keyed each block has duplicate key \`${value}\` at indexes ${a} and ${b}`
+ : `Keyed each block has duplicate key at indexes ${a} and ${b}`}\nhttps://svelte.dev/e/each_key_duplicate`);
error.name = 'Svelte error';
+
throw error;
} else {
throw new Error(`https://svelte.dev/e/each_key_duplicate`);
@@ -131,6 +139,7 @@ export function effect_in_teardown(rune) {
const error = new Error(`effect_in_teardown\n\`${rune}\` cannot be used inside an effect cleanup function\nhttps://svelte.dev/e/effect_in_teardown`);
error.name = 'Svelte error';
+
throw error;
} else {
throw new Error(`https://svelte.dev/e/effect_in_teardown`);
@@ -146,6 +155,7 @@ export function effect_in_unowned_derived() {
const error = new Error(`effect_in_unowned_derived\nEffect cannot be created inside a \`$derived\` value that was not itself created inside an effect\nhttps://svelte.dev/e/effect_in_unowned_derived`);
error.name = 'Svelte error';
+
throw error;
} else {
throw new Error(`https://svelte.dev/e/effect_in_unowned_derived`);
@@ -162,6 +172,7 @@ export function effect_orphan(rune) {
const error = new Error(`effect_orphan\n\`${rune}\` can only be used inside an effect (e.g. during component initialisation)\nhttps://svelte.dev/e/effect_orphan`);
error.name = 'Svelte error';
+
throw error;
} else {
throw new Error(`https://svelte.dev/e/effect_orphan`);
@@ -177,12 +188,29 @@ export function effect_update_depth_exceeded() {
const error = new Error(`effect_update_depth_exceeded\nMaximum update depth exceeded. This can happen when a reactive block or effect repeatedly sets a new value. Svelte limits the number of nested updates to prevent infinite loops\nhttps://svelte.dev/e/effect_update_depth_exceeded`);
error.name = 'Svelte error';
+
throw error;
} else {
throw new Error(`https://svelte.dev/e/effect_update_depth_exceeded`);
}
}
+/**
+ * `getAbortSignal()` can only be called inside an effect or derived
+ * @returns {never}
+ */
+export function get_abort_signal_outside_reaction() {
+ if (DEV) {
+ const error = new Error(`get_abort_signal_outside_reaction\n\`getAbortSignal()\` can only be called inside an effect or derived\nhttps://svelte.dev/e/get_abort_signal_outside_reaction`);
+
+ error.name = 'Svelte error';
+
+ throw error;
+ } else {
+ throw new Error(`https://svelte.dev/e/get_abort_signal_outside_reaction`);
+ }
+}
+
/**
* Failed to hydrate the application
* @returns {never}
@@ -192,6 +220,7 @@ export function hydration_failed() {
const error = new Error(`hydration_failed\nFailed to hydrate the application\nhttps://svelte.dev/e/hydration_failed`);
error.name = 'Svelte error';
+
throw error;
} else {
throw new Error(`https://svelte.dev/e/hydration_failed`);
@@ -207,6 +236,7 @@ export function invalid_snippet() {
const error = new Error(`invalid_snippet\nCould not \`{@render}\` snippet due to the expression being \`null\` or \`undefined\`. Consider using optional chaining \`{@render snippet?.()}\`\nhttps://svelte.dev/e/invalid_snippet`);
error.name = 'Svelte error';
+
throw error;
} else {
throw new Error(`https://svelte.dev/e/invalid_snippet`);
@@ -223,6 +253,7 @@ export function lifecycle_legacy_only(name) {
const error = new Error(`lifecycle_legacy_only\n\`${name}(...)\` cannot be used in runes mode\nhttps://svelte.dev/e/lifecycle_legacy_only`);
error.name = 'Svelte error';
+
throw error;
} else {
throw new Error(`https://svelte.dev/e/lifecycle_legacy_only`);
@@ -239,6 +270,7 @@ export function props_invalid_value(key) {
const error = new Error(`props_invalid_value\nCannot do \`bind:${key}={undefined}\` when \`${key}\` has a fallback value\nhttps://svelte.dev/e/props_invalid_value`);
error.name = 'Svelte error';
+
throw error;
} else {
throw new Error(`https://svelte.dev/e/props_invalid_value`);
@@ -255,6 +287,7 @@ export function props_rest_readonly(property) {
const error = new Error(`props_rest_readonly\nRest element properties of \`$props()\` such as \`${property}\` are readonly\nhttps://svelte.dev/e/props_rest_readonly`);
error.name = 'Svelte error';
+
throw error;
} else {
throw new Error(`https://svelte.dev/e/props_rest_readonly`);
@@ -271,6 +304,7 @@ export function rune_outside_svelte(rune) {
const error = new Error(`rune_outside_svelte\nThe \`${rune}\` rune is only available inside \`.svelte\` and \`.svelte.js/ts\` files\nhttps://svelte.dev/e/rune_outside_svelte`);
error.name = 'Svelte error';
+
throw error;
} else {
throw new Error(`https://svelte.dev/e/rune_outside_svelte`);
@@ -286,6 +320,7 @@ export function state_descriptors_fixed() {
const error = new Error(`state_descriptors_fixed\nProperty descriptors defined on \`$state\` objects must contain \`value\` and always be \`enumerable\`, \`configurable\` and \`writable\`.\nhttps://svelte.dev/e/state_descriptors_fixed`);
error.name = 'Svelte error';
+
throw error;
} else {
throw new Error(`https://svelte.dev/e/state_descriptors_fixed`);
@@ -301,36 +336,23 @@ export function state_prototype_fixed() {
const error = new Error(`state_prototype_fixed\nCannot set prototype of \`$state\` object\nhttps://svelte.dev/e/state_prototype_fixed`);
error.name = 'Svelte error';
- throw error;
- } else {
- throw new Error(`https://svelte.dev/e/state_prototype_fixed`);
- }
-}
-
-/**
- * Reading state that was created inside the same derived is forbidden. Consider using `untrack` to read locally created state
- * @returns {never}
- */
-export function state_unsafe_local_read() {
- if (DEV) {
- const error = new Error(`state_unsafe_local_read\nReading state that was created inside the same derived is forbidden. Consider using \`untrack\` to read locally created state\nhttps://svelte.dev/e/state_unsafe_local_read`);
- error.name = 'Svelte error';
throw error;
} else {
- throw new Error(`https://svelte.dev/e/state_unsafe_local_read`);
+ throw new Error(`https://svelte.dev/e/state_prototype_fixed`);
}
}
/**
- * Updating state inside a derived or a template expression is forbidden. If the value should not be reactive, declare it without `$state`
+ * Updating state inside `$derived(...)`, `$inspect(...)` or a template expression is forbidden. If the value should not be reactive, declare it without `$state`
* @returns {never}
*/
export function state_unsafe_mutation() {
if (DEV) {
- const error = new Error(`state_unsafe_mutation\nUpdating state inside a derived or a template expression is forbidden. If the value should not be reactive, declare it without \`$state\`\nhttps://svelte.dev/e/state_unsafe_mutation`);
+ const error = new Error(`state_unsafe_mutation\nUpdating state inside \`$derived(...)\`, \`$inspect(...)\` or a template expression is forbidden. If the value should not be reactive, declare it without \`$state\`\nhttps://svelte.dev/e/state_unsafe_mutation`);
error.name = 'Svelte error';
+
throw error;
} else {
throw new Error(`https://svelte.dev/e/state_unsafe_mutation`);
diff --git a/packages/svelte/src/internal/client/index.js b/packages/svelte/src/internal/client/index.js
index 31da00dbb448..576a30fa775c 100644
--- a/packages/svelte/src/internal/client/index.js
+++ b/packages/svelte/src/internal/client/index.js
@@ -1,21 +1,15 @@
+export { createAttachmentKey as attachment } from '../../attachments/index.js';
export { FILENAME, HMR, NAMESPACE_SVG } from '../../constants.js';
-export { push, pop } from './context.js';
+export { push, pop, add_svelte_meta } from './context.js';
export { assign, assign_and, assign_or, assign_nullish } from './dev/assign.js';
export { cleanup_styles } from './dev/css.js';
export { add_locations } from './dev/elements.js';
export { hmr } from './dev/hmr.js';
-export {
- ADD_OWNER,
- add_owner,
- mark_module_start,
- mark_module_end,
- add_owner_effect,
- add_owner_to_class,
- skip_ownership_validation
-} from './dev/ownership.js';
+export { create_ownership_validator } from './dev/ownership.js';
export { check_target, legacy_api } from './dev/legacy.js';
-export { trace } from './dev/tracing.js';
+export { trace, tag, tag_proxy } from './dev/tracing.js';
export { inspect } from './dev/inspect.js';
+export { validate_snippet_args } from './dev/validation.js';
export { await_block as await } from './dom/blocks/await.js';
export { if_block as if } from './dom/blocks/if.js';
export { key_block as key } from './dom/blocks/key.js';
@@ -29,10 +23,12 @@ export { element } from './dom/blocks/svelte-element.js';
export { head } from './dom/blocks/svelte-head.js';
export { append_styles } from './dom/css.js';
export { action } from './dom/elements/actions.js';
+export { attach } from './dom/elements/attachments.js';
export {
remove_input_defaults,
set_attribute,
set_attributes,
+ attribute_effect,
set_custom_element_data,
set_xlink_attribute,
set_value,
@@ -93,15 +89,15 @@ export {
export {
append,
comment,
- ns_template,
- svg_template_with_script,
- mathml_template,
- template,
- template_with_script,
+ from_html,
+ from_mathml,
+ from_svg,
+ from_tree,
text,
- props_id
+ props_id,
+ with_script
} from './dom/template.js';
-export { derived, derived_safe_equal } from './reactivity/deriveds.js';
+export { user_derived as derived, derived_safe_equal } from './reactivity/deriveds.js';
export {
effect_tracking,
effect_root,
@@ -113,7 +109,7 @@ export {
user_effect,
user_pre_effect
} from './reactivity/effects.js';
-export { mutable_state, mutate, set, state, update, update_pre } from './reactivity/sources.js';
+export { mutable_source, mutate, set, state, update, update_pre } from './reactivity/sources.js';
export {
prop,
rest_props,
@@ -159,12 +155,13 @@ export {
} from './dom/operations.js';
export { attr, clsx } from '../shared/attributes.js';
export { snapshot } from '../shared/clone.js';
-export { noop, fallback } from '../shared/utils.js';
+export { noop, fallback, to_array } from '../shared/utils.js';
export {
invalid_default_snippet,
validate_dynamic_element_tag,
validate_store,
- validate_void_dynamic_element
+ validate_void_dynamic_element,
+ prevent_snippet_stringification
} from '../shared/validate.js';
export { strict_equals, equals } from './dev/equality.js';
export { log_if_contains_state } from './dev/console-log.js';
diff --git a/packages/svelte/src/internal/client/proxy.js b/packages/svelte/src/internal/client/proxy.js
index 4c262880f1cd..d9063aee3436 100644
--- a/packages/svelte/src/internal/client/proxy.js
+++ b/packages/svelte/src/internal/client/proxy.js
@@ -1,7 +1,6 @@
-/** @import { ProxyMetadata, Source } from '#client' */
+/** @import { Source } from '#client' */
import { DEV } from 'esm-env';
-import { get, active_effect } from './runtime.js';
-import { component_context } from './context.js';
+import { get, active_effect, active_reaction, set_active_reaction } from './runtime.js';
import {
array_prototype,
get_descriptor,
@@ -9,27 +8,22 @@ import {
is_array,
object_prototype
} from '../shared/utils.js';
-import { check_ownership, widen_ownership } from './dev/ownership.js';
-import { source, set } from './reactivity/sources.js';
-import { STATE_SYMBOL, STATE_SYMBOL_METADATA } from './constants.js';
+import { state as source, set } from './reactivity/sources.js';
+import { PROXY_PATH_SYMBOL, STATE_SYMBOL } from '#client/constants';
import { UNINITIALIZED } from '../../constants.js';
import * as e from './errors.js';
-import { get_stack } from './dev/tracing.js';
+import { get_stack, tag } from './dev/tracing.js';
import { tracing_mode_flag } from '../flags/index.js';
+// TODO move all regexes into shared module?
+const regex_is_valid_identifier = /^[a-zA-Z_$][a-zA-Z_$0-9]*$/;
+
/**
* @template T
* @param {T} value
- * @param {ProxyMetadata | null} [parent]
- * @param {Source} [prev] dev mode only
* @returns {T}
*/
-export function proxy(value, parent = null, prev) {
- /** @type {Error | null} */
- var stack = null;
- if (DEV && tracing_mode_flag) {
- stack = get_stack('CreatedAt');
- }
+export function proxy(value) {
// if non-proxyable, or is already a proxy, return `value`
if (typeof value !== 'object' || value === null || STATE_SYMBOL in value) {
return value;
@@ -46,34 +40,43 @@ export function proxy(value, parent = null, prev) {
var is_proxied_array = is_array(value);
var version = source(0);
+ var stack = DEV && tracing_mode_flag ? get_stack('CreatedAt') : null;
+ var reaction = active_reaction;
+
+ /**
+ * Executes the proxy in the context of the reaction it was originally created in, if any
+ * @template T
+ * @param {() => T} fn
+ */
+ var with_parent = (fn) => {
+ var previous_reaction = active_reaction;
+ set_active_reaction(reaction);
+
+ /** @type {T} */
+ var result = fn();
+
+ set_active_reaction(previous_reaction);
+ return result;
+ };
+
if (is_proxied_array) {
// We need to create the length source eagerly to ensure that
// mutations to the array are properly synced with our proxy
sources.set('length', source(/** @type {any[]} */ (value).length, stack));
}
- /** @type {ProxyMetadata} */
- var metadata;
-
- if (DEV) {
- metadata = {
- parent,
- owners: null
- };
-
- if (prev) {
- // Reuse owners from previous state; necessary because reassignment is not guaranteed to have correct component context.
- // If no previous proxy exists we play it safe and assume ownerless state
- // @ts-expect-error
- const prev_owners = prev.v?.[STATE_SYMBOL_METADATA]?.owners;
- metadata.owners = prev_owners ? new Set(prev_owners) : null;
- } else {
- metadata.owners =
- parent === null
- ? component_context !== null
- ? new Set([component_context.function])
- : null
- : new Set();
+ /** Used in dev for $inspect.trace() */
+ var path = '';
+
+ /** @param {string} new_path */
+ function update_path(new_path) {
+ path = new_path;
+
+ tag(version, `${path} version`);
+
+ // rename all child sources and child proxies
+ for (const [prop, source] of sources) {
+ tag(source, get_label(path, prop));
}
}
@@ -91,14 +94,18 @@ export function proxy(value, parent = null, prev) {
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/Proxy/getOwnPropertyDescriptor#invariants
e.state_descriptors_fixed();
}
-
var s = sources.get(prop);
-
if (s === undefined) {
- s = source(descriptor.value, stack);
- sources.set(prop, s);
+ s = with_parent(() => {
+ var s = source(descriptor.value, stack);
+ sources.set(prop, s);
+ if (DEV && typeof prop === 'string') {
+ tag(s, get_label(path, prop));
+ }
+ return s;
+ });
} else {
- set(s, proxy(descriptor.value, metadata));
+ set(s, descriptor.value, true);
}
return true;
@@ -109,7 +116,13 @@ export function proxy(value, parent = null, prev) {
if (s === undefined) {
if (prop in target) {
- sources.set(prop, source(UNINITIALIZED, stack));
+ const s = with_parent(() => source(UNINITIALIZED, stack));
+ sources.set(prop, s);
+ update_version(version);
+
+ if (DEV) {
+ tag(s, get_label(path, prop));
+ }
}
} else {
// When working with arrays, we need to also ensure we update the length when removing
@@ -130,41 +143,35 @@ export function proxy(value, parent = null, prev) {
},
get(target, prop, receiver) {
- if (DEV && prop === STATE_SYMBOL_METADATA) {
- return metadata;
- }
-
if (prop === STATE_SYMBOL) {
return value;
}
+ if (DEV && prop === PROXY_PATH_SYMBOL) {
+ return update_path;
+ }
+
var s = sources.get(prop);
var exists = prop in target;
// create a source, but only if it's an own property and not a prototype property
if (s === undefined && (!exists || get_descriptor(target, prop)?.writable)) {
- s = source(proxy(exists ? target[prop] : UNINITIALIZED, metadata), stack);
+ s = with_parent(() => {
+ var p = proxy(exists ? target[prop] : UNINITIALIZED);
+ var s = source(p, stack);
+
+ if (DEV) {
+ tag(s, get_label(path, prop));
+ }
+
+ return s;
+ });
+
sources.set(prop, s);
}
if (s !== undefined) {
var v = get(s);
-
- // In case of something like `foo = bar.map(...)`, foo would have ownership
- // of the array itself, while the individual items would have ownership
- // of the component that created bar. That means if we later do `foo[0].baz = 42`,
- // we could get a false-positive ownership violation, since the two proxies
- // are not connected to each other via the parent metadata relationship.
- // For this reason, we need to widen the ownership of the children
- // upon access when we detect they are not connected.
- if (DEV) {
- /** @type {ProxyMetadata | undefined} */
- var prop_metadata = v?.[STATE_SYMBOL_METADATA];
- if (prop_metadata && prop_metadata?.parent !== metadata) {
- widen_ownership(metadata, prop_metadata);
- }
- }
-
return v === UNINITIALIZED ? undefined : v;
}
@@ -195,10 +202,6 @@ export function proxy(value, parent = null, prev) {
},
has(target, prop) {
- if (DEV && prop === STATE_SYMBOL_METADATA) {
- return true;
- }
-
if (prop === STATE_SYMBOL) {
return true;
}
@@ -211,7 +214,17 @@ export function proxy(value, parent = null, prev) {
(active_effect !== null && (!has || get_descriptor(target, prop)?.writable))
) {
if (s === undefined) {
- s = source(has ? proxy(target[prop], metadata) : UNINITIALIZED, stack);
+ s = with_parent(() => {
+ var p = has ? proxy(target[prop]) : UNINITIALIZED;
+ var s = source(p, stack);
+
+ if (DEV) {
+ tag(s, get_label(path, prop));
+ }
+
+ return s;
+ });
+
sources.set(prop, s);
}
@@ -238,8 +251,12 @@ export function proxy(value, parent = null, prev) {
// If the item exists in the original, we need to create a uninitialized source,
// else a later read of the property would result in a source being created with
// the value of the original item at that index.
- other_s = source(UNINITIALIZED, stack);
+ other_s = with_parent(() => source(UNINITIALIZED, stack));
sources.set(i + '', other_s);
+
+ if (DEV) {
+ tag(other_s, get_label(path, i));
+ }
}
}
}
@@ -250,22 +267,20 @@ export function proxy(value, parent = null, prev) {
// object property before writing to that property.
if (s === undefined) {
if (!has || get_descriptor(target, prop)?.writable) {
- s = source(undefined, stack);
- set(s, proxy(value, metadata));
+ s = with_parent(() => source(undefined, stack));
+ set(s, proxy(value));
+
sources.set(prop, s);
+
+ if (DEV) {
+ tag(s, get_label(path, prop));
+ }
}
} else {
has = s.v !== UNINITIALIZED;
- set(s, proxy(value, metadata));
- }
- if (DEV) {
- /** @type {ProxyMetadata | undefined} */
- var prop_metadata = value?.[STATE_SYMBOL_METADATA];
- if (prop_metadata && prop_metadata?.parent !== metadata) {
- widen_ownership(metadata, prop_metadata);
- }
- check_ownership(metadata);
+ var p = with_parent(() => proxy(value));
+ set(s, p);
}
var descriptor = Reflect.getOwnPropertyDescriptor(target, prop);
@@ -318,6 +333,16 @@ export function proxy(value, parent = null, prev) {
});
}
+/**
+ * @param {string} path
+ * @param {string | symbol} prop
+ */
+function get_label(path, prop) {
+ if (typeof prop === 'symbol') return `${path}[Symbol(${prop.description ?? ''})]`;
+ if (regex_is_valid_identifier.test(prop)) return `${path}.${prop}`;
+ return /^\d+$/.test(prop) ? `${path}[${prop}]` : `${path}['${prop}']`;
+}
+
/**
* @param {Source} signal
* @param {1 | -1} [d]
@@ -330,8 +355,18 @@ function update_version(signal, d = 1) {
* @param {any} value
*/
export function get_proxied_value(value) {
- if (value !== null && typeof value === 'object' && STATE_SYMBOL in value) {
- return value[STATE_SYMBOL];
+ try {
+ if (value !== null && typeof value === 'object' && STATE_SYMBOL in value) {
+ return value[STATE_SYMBOL];
+ }
+ } catch {
+ // the above if check can throw an error if the value in question
+ // is the contentWindow of an iframe on another domain, in which
+ // case we want to just return the value (because it's definitely
+ // not a proxied value) so we don't break any JavaScript interacting
+ // with that iframe (such as various payment companies client side
+ // JavaScript libraries interacting with their iframes on the same
+ // domain)
}
return value;
diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js
index 795417cc0fdb..fdfc13c0e20a 100644
--- a/packages/svelte/src/internal/client/reactivity/deriveds.js
+++ b/packages/svelte/src/internal/client/reactivity/deriveds.js
@@ -1,6 +1,6 @@
/** @import { Derived, Effect } from '#client' */
import { DEV } from 'esm-env';
-import { CLEAN, DERIVED, DIRTY, EFFECT_HAS_DERIVED, MAYBE_DIRTY, UNOWNED } from '../constants.js';
+import { CLEAN, DERIVED, DIRTY, EFFECT_PRESERVED, MAYBE_DIRTY, UNOWNED } from '#client/constants';
import {
active_reaction,
active_effect,
@@ -8,7 +8,9 @@ import {
skip_reaction,
update_reaction,
increment_write_version,
- set_active_effect
+ set_active_effect,
+ push_reaction_value,
+ is_destroying_effect
} from '../runtime.js';
import { equals, safe_equals } from './equality.js';
import * as e from '../errors.js';
@@ -36,7 +38,7 @@ export function derived(fn) {
} else {
// Since deriveds are evaluated lazily, any effects created inside them are
// created too late to ensure that the parent effect is added to the tree
- active_effect.f |= EFFECT_HAS_DERIVED;
+ active_effect.f |= EFFECT_PRESERVED;
}
/** @type {Derived} */
@@ -51,7 +53,8 @@ export function derived(fn) {
rv: 0,
v: /** @type {V} */ (null),
wv: 0,
- parent: parent_derived ?? active_effect
+ parent: parent_derived ?? active_effect,
+ ac: null
};
if (DEV && tracing_mode_flag) {
@@ -61,6 +64,20 @@ export function derived(fn) {
return signal;
}
+/**
+ * @template V
+ * @param {() => V} fn
+ * @returns {Derived}
+ */
+/*#__NO_SIDE_EFFECTS__*/
+export function user_derived(fn) {
+ const d = derived(fn);
+
+ push_reaction_value(d);
+
+ return d;
+}
+
/**
* @template V
* @param {() => V} fn
@@ -116,7 +133,7 @@ function get_derived_parent_effect(derived) {
* @param {Derived} derived
* @returns {T}
*/
-function execute_derived(derived) {
+export function execute_derived(derived) {
var value;
var prev_active_effect = active_effect;
@@ -157,13 +174,18 @@ function execute_derived(derived) {
*/
export function update_derived(derived) {
var value = execute_derived(derived);
- var status =
- (skip_reaction || (derived.f & UNOWNED) !== 0) && derived.deps !== null ? MAYBE_DIRTY : CLEAN;
-
- set_signal_status(derived, status);
if (!derived.equals(value)) {
derived.v = value;
derived.wv = increment_write_version();
}
+
+ // don't mark derived clean if we're reading it inside a
+ // cleanup function, or it will cache a stale value
+ if (is_destroying_effect) return;
+
+ var status =
+ (skip_reaction || (derived.f & UNOWNED) !== 0) && derived.deps !== null ? MAYBE_DIRTY : CLEAN;
+
+ set_signal_status(derived, status);
}
diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js
index 28589ce94df1..7570064c37c7 100644
--- a/packages/svelte/src/internal/client/reactivity/effects.js
+++ b/packages/svelte/src/internal/client/reactivity/effects.js
@@ -31,16 +31,17 @@ import {
INSPECT_EFFECT,
HEAD_EFFECT,
MAYBE_DIRTY,
- EFFECT_HAS_DERIVED,
- BOUNDARY_EFFECT
-} from '../constants.js';
+ EFFECT_PRESERVED,
+ BOUNDARY_EFFECT,
+ STALE_REACTION
+} from '#client/constants';
import { set } from './sources.js';
import * as e from '../errors.js';
import { DEV } from 'esm-env';
import { define_property } from '../../shared/utils.js';
import { get_next_sibling } from '../dom/operations.js';
import { derived } from './deriveds.js';
-import { component_context, dev_current_component_function } from '../context.js';
+import { component_context, dev_current_component_function, dev_stack } from '../context.js';
/**
* @param {'$effect' | '$effect.pre' | '$inspect'} rune
@@ -82,13 +83,12 @@ function push_effect(effect, parent_effect) {
* @returns {Effect}
*/
function create_effect(type, fn, sync, push = true) {
- var is_root = (type & ROOT_EFFECT) !== 0;
- var parent_effect = active_effect;
+ var parent = active_effect;
if (DEV) {
// Ensure the parent is never an inspect effect
- while (parent_effect !== null && (parent_effect.f & INSPECT_EFFECT) !== 0) {
- parent_effect = parent_effect.parent;
+ while (parent !== null && (parent.f & INSPECT_EFFECT) !== 0) {
+ parent = parent.parent;
}
}
@@ -103,11 +103,13 @@ function create_effect(type, fn, sync, push = true) {
fn,
last: null,
next: null,
- parent: is_root ? null : parent_effect,
+ parent,
+ b: parent && parent.b,
prev: null,
teardown: null,
transitions: null,
- wv: 0
+ wv: 0,
+ ac: null
};
if (DEV) {
@@ -134,11 +136,11 @@ function create_effect(type, fn, sync, push = true) {
effect.first === null &&
effect.nodes_start === null &&
effect.teardown === null &&
- (effect.f & (EFFECT_HAS_DERIVED | BOUNDARY_EFFECT)) === 0;
+ (effect.f & (EFFECT_PRESERVED | BOUNDARY_EFFECT)) === 0;
- if (!inert && !is_root && push) {
- if (parent_effect !== null) {
- push_effect(effect, parent_effect);
+ if (!inert && push) {
+ if (parent !== null) {
+ push_effect(effect, parent);
}
// if we're in a derived, add the effect there too
@@ -330,19 +332,27 @@ export function render_effect(fn) {
/**
* @param {(...expressions: any) => void | (() => void)} fn
* @param {Array<() => any>} thunks
+ * @param {(fn: () => T) => Derived} d
* @returns {Effect}
*/
export function template_effect(fn, thunks = [], d = derived) {
- const deriveds = thunks.map(d);
- const effect = () => fn(...deriveds.map(get));
-
if (DEV) {
- define_property(effect, 'name', {
- value: '{expression}'
+ // wrap the effect so that we can decorate stack trace with `in {expression}`
+ // (TODO maybe there's a better approach?)
+ return render_effect(() => {
+ var outer = /** @type {Effect} */ (active_effect);
+ var inner = () => fn(...deriveds.map(get));
+
+ define_property(outer.fn, 'name', { value: '{expression}' });
+ define_property(inner, 'name', { value: '{expression}' });
+
+ const deriveds = thunks.map(d);
+ block(inner);
});
}
- return block(effect);
+ const deriveds = thunks.map(d);
+ return block(() => fn(...deriveds.map(get)));
}
/**
@@ -350,7 +360,11 @@ export function template_effect(fn, thunks = [], d = derived) {
* @param {number} flags
*/
export function block(fn, flags = 0) {
- return create_effect(RENDER_EFFECT | BLOCK_EFFECT | flags, fn, true);
+ var effect = create_effect(RENDER_EFFECT | BLOCK_EFFECT | flags, fn, true);
+ if (DEV) {
+ effect.dev_stack = dev_stack;
+ }
+ return effect;
}
/**
@@ -390,8 +404,17 @@ export function destroy_effect_children(signal, remove_dom = false) {
signal.first = signal.last = null;
while (effect !== null) {
+ effect.ac?.abort(STALE_REACTION);
+
var next = effect.next;
- destroy_effect(effect, remove_dom);
+
+ if ((effect.f & ROOT_EFFECT) !== 0) {
+ // this is now an independent root
+ effect.parent = null;
+ } else {
+ destroy_effect(effect, remove_dom);
+ }
+
effect = next;
}
}
@@ -420,19 +443,12 @@ export function destroy_block_effect_children(signal) {
export function destroy_effect(effect, remove_dom = true) {
var removed = false;
- if ((remove_dom || (effect.f & HEAD_EFFECT) !== 0) && effect.nodes_start !== null) {
- /** @type {TemplateNode | null} */
- var node = effect.nodes_start;
- var end = effect.nodes_end;
-
- while (node !== null) {
- /** @type {TemplateNode | null} */
- var next = node === end ? null : /** @type {TemplateNode} */ (get_next_sibling(node));
-
- node.remove();
- node = next;
- }
-
+ if (
+ (remove_dom || (effect.f & HEAD_EFFECT) !== 0) &&
+ effect.nodes_start !== null &&
+ effect.nodes_end !== null
+ ) {
+ remove_effect_dom(effect.nodes_start, /** @type {TemplateNode} */ (effect.nodes_end));
removed = true;
}
@@ -471,9 +487,25 @@ export function destroy_effect(effect, remove_dom = true) {
effect.fn =
effect.nodes_start =
effect.nodes_end =
+ effect.ac =
null;
}
+/**
+ *
+ * @param {TemplateNode | null} node
+ * @param {TemplateNode} end
+ */
+export function remove_effect_dom(node, end) {
+ while (node !== null) {
+ /** @type {TemplateNode | null} */
+ var next = node === end ? null : /** @type {TemplateNode} */ (get_next_sibling(node));
+
+ node.remove();
+ node = next;
+ }
+}
+
/**
* Detach an effect from the effect tree, freeing up memory and
* reducing the amount of work that happens on subsequent traversals
@@ -577,19 +609,6 @@ function resume_children(effect, local) {
if ((effect.f & INERT) === 0) return;
effect.f ^= INERT;
- // Ensure the effect is marked as clean again so that any dirty child
- // effects can schedule themselves for execution
- if ((effect.f & CLEAN) === 0) {
- effect.f ^= CLEAN;
- }
-
- // If a dependency of this effect changed while it was paused,
- // schedule the effect to update
- if (check_dirtiness(effect)) {
- set_signal_status(effect, DIRTY);
- schedule_effect(effect);
- }
-
var child = effect.first;
while (child !== null) {
diff --git a/packages/svelte/src/internal/client/reactivity/equality.js b/packages/svelte/src/internal/client/reactivity/equality.js
index 37a9994ab8cc..104123857366 100644
--- a/packages/svelte/src/internal/client/reactivity/equality.js
+++ b/packages/svelte/src/internal/client/reactivity/equality.js
@@ -1,4 +1,5 @@
/** @import { Equals } from '#client' */
+
/** @type {Equals} */
export function equals(value) {
return value === this.v;
diff --git a/packages/svelte/src/internal/client/reactivity/props.js b/packages/svelte/src/internal/client/reactivity/props.js
index 5a3b30281f9f..3501bcd3c722 100644
--- a/packages/svelte/src/internal/client/reactivity/props.js
+++ b/packages/svelte/src/internal/client/reactivity/props.js
@@ -1,4 +1,4 @@
-/** @import { Source } from './types.js' */
+/** @import { Derived, Source } from './types.js' */
import { DEV } from 'esm-env';
import {
PROPS_IS_BINDABLE,
@@ -8,26 +8,11 @@ import {
PROPS_IS_UPDATED
} from '../../../constants.js';
import { get_descriptor, is_function } from '../../shared/utils.js';
-import { mutable_source, set, source, update } from './sources.js';
+import { set, source, update } from './sources.js';
import { derived, derived_safe_equal } from './deriveds.js';
-import {
- active_effect,
- get,
- captured_signals,
- set_active_effect,
- untrack,
- active_reaction,
- set_active_reaction
-} from '../runtime.js';
-import { safe_equals } from './equality.js';
+import { get, untrack } from '../runtime.js';
import * as e from '../errors.js';
-import {
- BRANCH_EFFECT,
- LEGACY_DERIVED_PROP,
- LEGACY_PROPS,
- ROOT_EFFECT,
- STATE_SYMBOL
-} from '../constants.js';
+import { LEGACY_PROPS, STATE_SYMBOL } from '#client/constants';
import { proxy } from '../proxy.js';
import { capture_store_binding } from './store.js';
import { legacy_mode_flag } from '../../flags/index.js';
@@ -232,9 +217,15 @@ const spread_props_handler = {
for (let p of target.props) {
if (is_function(p)) p = p();
+ if (!p) continue;
+
for (const key in p) {
if (!keys.includes(key)) keys.push(key);
}
+
+ for (const key of Object.getOwnPropertySymbols(p)) {
+ if (!keys.includes(key)) keys.push(key);
+ }
}
return keys;
@@ -249,6 +240,14 @@ export function spread_props(...props) {
return new Proxy({ props }, spread_props_handler);
}
+/**
+ * @param {Derived} current_value
+ * @returns {boolean}
+ */
+function has_destroyed_component_ctx(current_value) {
+ return current_value.ctx?.d ?? false;
+}
+
/**
* This function is responsible for synchronizing a possibly bound prop with the inner component state.
* It is used whenever the compiler sees that the component writes to the prop, or when it has a default prop_value.
@@ -260,89 +259,92 @@ export function spread_props(...props) {
* @returns {(() => V | ((arg: V) => V) | ((arg: V, mutation: boolean) => V))}
*/
export function prop(props, key, flags, fallback) {
- var immutable = (flags & PROPS_IS_IMMUTABLE) !== 0;
var runes = !legacy_mode_flag || (flags & PROPS_IS_RUNES) !== 0;
var bindable = (flags & PROPS_IS_BINDABLE) !== 0;
var lazy = (flags & PROPS_IS_LAZY_INITIAL) !== 0;
- var is_store_sub = false;
- var prop_value;
-
- if (bindable) {
- [prop_value, is_store_sub] = capture_store_binding(() => /** @type {V} */ (props[key]));
- } else {
- prop_value = /** @type {V} */ (props[key]);
- }
-
- // Can be the case when someone does `mount(Component, props)` with `let props = $state({...})`
- // or `createClassComponent(Component, props)`
- var is_entry_props = STATE_SYMBOL in props || LEGACY_PROPS in props;
-
- var setter =
- (bindable &&
- (get_descriptor(props, key)?.set ??
- (is_entry_props && key in props && ((v) => (props[key] = v))))) ||
- undefined;
var fallback_value = /** @type {V} */ (fallback);
var fallback_dirty = true;
- var fallback_used = false;
var get_fallback = () => {
- fallback_used = true;
if (fallback_dirty) {
fallback_dirty = false;
- if (lazy) {
- fallback_value = untrack(/** @type {() => V} */ (fallback));
- } else {
- fallback_value = /** @type {V} */ (fallback);
- }
+
+ fallback_value = lazy
+ ? untrack(/** @type {() => V} */ (fallback))
+ : /** @type {V} */ (fallback);
}
return fallback_value;
};
- if (prop_value === undefined && fallback !== undefined) {
- if (setter && runes) {
- e.props_invalid_value(key);
- }
+ /** @type {((v: V) => void) | undefined} */
+ var setter;
+
+ if (bindable) {
+ // Can be the case when someone does `mount(Component, props)` with `let props = $state({...})`
+ // or `createClassComponent(Component, props)`
+ var is_entry_props = STATE_SYMBOL in props || LEGACY_PROPS in props;
+
+ setter =
+ get_descriptor(props, key)?.set ??
+ (is_entry_props && key in props ? (v) => (props[key] = v) : undefined);
+ }
+
+ var initial_value;
+ var is_store_sub = false;
- prop_value = get_fallback();
- if (setter) setter(prop_value);
+ if (bindable) {
+ [initial_value, is_store_sub] = capture_store_binding(() => /** @type {V} */ (props[key]));
+ } else {
+ initial_value = /** @type {V} */ (props[key]);
+ }
+
+ if (initial_value === undefined && fallback !== undefined) {
+ initial_value = get_fallback();
+
+ if (setter) {
+ if (runes) e.props_invalid_value(key);
+ setter(initial_value);
+ }
}
/** @type {() => V} */
var getter;
+
if (runes) {
getter = () => {
var value = /** @type {V} */ (props[key]);
if (value === undefined) return get_fallback();
fallback_dirty = true;
- fallback_used = false;
return value;
};
} else {
- // Svelte 4 did not trigger updates when a primitive value was updated to the same value.
- // Replicate that behavior through using a derived
- var derived_getter = (immutable ? derived : derived_safe_equal)(
- () => /** @type {V} */ (props[key])
- );
- derived_getter.f |= LEGACY_DERIVED_PROP;
getter = () => {
- var value = get(derived_getter);
- if (value !== undefined) fallback_value = /** @type {V} */ (undefined);
+ var value = /** @type {V} */ (props[key]);
+
+ if (value !== undefined) {
+ // in legacy mode, we don't revert to the fallback value
+ // if the prop goes from defined to undefined. The easiest
+ // way to model this is to make the fallback undefined
+ // as soon as the prop has a value
+ fallback_value = /** @type {V} */ (undefined);
+ }
+
return value === undefined ? fallback_value : value;
};
}
- // easy mode — prop is never written to
- if ((flags & PROPS_IS_UPDATED) === 0) {
+ // prop is never written to — we only need a getter
+ if (runes && (flags & PROPS_IS_UPDATED) === 0) {
return getter;
}
- // intermediate mode — prop is written to, but the parent component had
- // `bind:foo` which means we can just call `$$props.foo = value` directly
+ // prop is written to, but the parent component had `bind:foo` which
+ // means we can just call `$$props.foo = value` directly
if (setter) {
var legacy_parent = props.$$legacy;
+
return function (/** @type {any} */ value, /** @type {boolean} */ mutation) {
if (arguments.length > 0) {
// We don't want to notify if the value was mutated and the parent is in runes mode.
@@ -352,67 +354,41 @@ export function prop(props, key, flags, fallback) {
if (!runes || !mutation || legacy_parent || is_store_sub) {
/** @type {Function} */ (setter)(mutation ? getter() : value);
}
+
return value;
- } else {
- return getter();
}
+
+ return getter();
};
}
- // hard mode. this is where it gets ugly — the value in the child should
- // synchronize with the parent, but it should also be possible to temporarily
- // set the value to something else locally.
- var from_child = false;
- var was_from_child = false;
-
- // The derived returns the current value. The underlying mutable
- // source is written to from various places to persist this value.
- var inner_current_value = mutable_source(prop_value);
- var current_value = derived(() => {
- var parent_value = getter();
- var child_value = get(inner_current_value);
-
- if (from_child) {
- from_child = false;
- was_from_child = true;
- return child_value;
- }
-
- was_from_child = false;
- return (inner_current_value.v = parent_value);
- });
+ // Either prop is written to, but there's no binding, which means we
+ // create a derived that we can write to locally.
+ // Or we are in legacy mode where we always create a derived to replicate that
+ // Svelte 4 did not trigger updates when a primitive value was updated to the same value.
+ var d = ((flags & PROPS_IS_IMMUTABLE) !== 0 ? derived : derived_safe_equal)(getter);
- if (!immutable) current_value.equals = safe_equals;
+ // Capture the initial value if it's bindable
+ if (bindable) get(d);
return function (/** @type {any} */ value, /** @type {boolean} */ mutation) {
- // legacy nonsense — need to ensure the source is invalidated when necessary
- // also needed for when handling inspect logic so we can inspect the correct source signal
- if (captured_signals !== null) {
- // set this so that we don't reset to the parent value if `d`
- // is invalidated because of `invalidate_inner_signals` (rather
- // than because the parent or child value changed)
- from_child = was_from_child;
- // invoke getters so that signals are picked up by `invalidate_inner_signals`
- getter();
- get(inner_current_value);
- }
-
if (arguments.length > 0) {
- const new_value = mutation ? get(current_value) : runes && bindable ? proxy(value) : value;
-
- if (!current_value.equals(new_value)) {
- from_child = true;
- set(inner_current_value, new_value);
- // To ensure the fallback value is consistent when used with proxies, we
- // update the local fallback_value, but only if the fallback is actively used
- if (fallback_used && fallback_value !== undefined) {
- fallback_value = new_value;
- }
- untrack(() => get(current_value)); // force a synchronisation immediately
+ const new_value = mutation ? get(d) : runes && bindable ? proxy(value) : value;
+
+ set(d, new_value);
+
+ if (fallback_value !== undefined) {
+ fallback_value = new_value;
}
return value;
}
- return get(current_value);
+
+ // TODO is this still necessary post-#16263?
+ if (has_destroyed_component_ctx(d)) {
+ return d.v;
+ }
+
+ return get(d);
};
}
diff --git a/packages/svelte/src/internal/client/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js
index f6a3fd7e330a..a86ccfee4ff4 100644
--- a/packages/svelte/src/internal/client/reactivity/sources.js
+++ b/packages/svelte/src/internal/client/reactivity/sources.js
@@ -1,4 +1,4 @@
-/** @import { Derived, Effect, Reaction, Source, Value } from '#client' */
+/** @import { Derived, Effect, Source, Value } from '#client' */
import { DEV } from 'esm-env';
import {
active_reaction,
@@ -11,10 +11,11 @@ import {
untrack,
increment_write_version,
update_effect,
- derived_sources,
- set_derived_sources,
+ source_ownership,
check_dirtiness,
- untracking
+ untracking,
+ is_destroying_effect,
+ push_reaction_value
} from '../runtime.js';
import { equals, safe_equals } from './equality.js';
import {
@@ -27,14 +28,19 @@ import {
MAYBE_DIRTY,
BLOCK_EFFECT,
ROOT_EFFECT
-} from '../constants.js';
+} from '#client/constants';
import * as e from '../errors.js';
import { legacy_mode_flag, tracing_mode_flag } from '../../flags/index.js';
-import { get_stack } from '../dev/tracing.js';
+import { get_stack, tag_proxy } from '../dev/tracing.js';
import { component_context, is_runes } from '../context.js';
+import { proxy } from '../proxy.js';
+import { execute_derived } from './deriveds.js';
export let inspect_effects = new Set();
+/** @type {Map} */
+export const old_values = new Map();
+
/**
* @param {Set} v
*/
@@ -48,6 +54,7 @@ export function set_inspect_effects(v) {
* @param {Error | null} [stack]
* @returns {Source}
*/
+// TODO rename this to `state` throughout the codebase
export function source(v, stack) {
/** @type {Value} */
var signal = {
@@ -61,7 +68,9 @@ export function source(v, stack) {
if (DEV && tracing_mode_flag) {
signal.created = stack ?? get_stack('CreatedAt');
- signal.debug = null;
+ signal.updated = null;
+ signal.set_during_effect = false;
+ signal.trace = null;
}
return signal;
@@ -70,9 +79,15 @@ export function source(v, stack) {
/**
* @template V
* @param {V} v
+ * @param {Error | null} [stack]
*/
-export function state(v) {
- return push_derived_source(source(v));
+/*#__NO_SIDE_EFFECTS__*/
+export function state(v, stack) {
+ const s = source(v, stack);
+
+ push_reaction_value(s);
+
+ return s;
}
/**
@@ -82,7 +97,7 @@ export function state(v) {
* @returns {Source}
*/
/*#__NO_SIDE_EFFECTS__*/
-export function mutable_source(initial_value, immutable = false) {
+export function mutable_source(initial_value, immutable = false, trackable = true) {
const s = source(initial_value);
if (!immutable) {
s.equals = safe_equals;
@@ -90,40 +105,13 @@ export function mutable_source(initial_value, immutable = false) {
// bind the signal to the component context, in case we need to
// track updates to trigger beforeUpdate/afterUpdate callbacks
- if (legacy_mode_flag && component_context !== null && component_context.l !== null) {
+ if (legacy_mode_flag && trackable && component_context !== null && component_context.l !== null) {
(component_context.l.s ??= []).push(s);
}
return s;
}
-/**
- * @template V
- * @param {V} v
- * @param {boolean} [immutable]
- * @returns {Source}
- */
-export function mutable_state(v, immutable = false) {
- return push_derived_source(mutable_source(v, immutable));
-}
-
-/**
- * @template V
- * @param {Source} source
- */
-/*#__NO_SIDE_EFFECTS__*/
-function push_derived_source(source) {
- if (active_reaction !== null && !untracking && (active_reaction.f & DERIVED) !== 0) {
- if (derived_sources === null) {
- set_derived_sources([source]);
- } else {
- derived_sources.push(source);
- }
- }
-
- return source;
-}
-
/**
* @template V
* @param {Value} source
@@ -141,22 +129,29 @@ export function mutate(source, value) {
* @template V
* @param {Source} source
* @param {V} value
+ * @param {boolean} [should_proxy]
* @returns {V}
*/
-export function set(source, value) {
+export function set(source, value, should_proxy = false) {
if (
active_reaction !== null &&
- !untracking &&
+ // since we are untracking the function inside `$inspect.with` we need to add this check
+ // to ensure we error if state is set inside an inspect effect
+ (!untracking || (active_reaction.f & INSPECT_EFFECT) !== 0) &&
is_runes() &&
- (active_reaction.f & (DERIVED | BLOCK_EFFECT)) !== 0 &&
- // If the source was created locally within the current derived, then
- // we allow the mutation.
- (derived_sources === null || !derived_sources.includes(source))
+ (active_reaction.f & (DERIVED | BLOCK_EFFECT | INSPECT_EFFECT)) !== 0 &&
+ !(source_ownership?.reaction === active_reaction && source_ownership.sources.includes(source))
) {
e.state_unsafe_mutation();
}
- return internal_set(source, value);
+ let new_value = should_proxy ? proxy(value) : value;
+
+ if (DEV) {
+ tag_proxy(new_value, /** @type {string} */ (source.label));
+ }
+
+ return internal_set(source, new_value);
}
/**
@@ -168,17 +163,33 @@ export function set(source, value) {
export function internal_set(source, value) {
if (!source.equals(value)) {
var old_value = source.v;
+
+ if (is_destroying_effect) {
+ old_values.set(source, value);
+ } else {
+ old_values.set(source, old_value);
+ }
+
source.v = value;
- source.wv = increment_write_version();
if (DEV && tracing_mode_flag) {
source.updated = get_stack('UpdatedAt');
- if (active_effect != null) {
- source.trace_need_increase = true;
- source.trace_v ??= old_value;
+
+ if (active_effect !== null) {
+ source.set_during_effect = true;
}
}
+ if ((source.f & DERIVED) !== 0) {
+ // if we are assigning to a dirty derived we set it to clean/maybe dirty but we also eagerly execute it to track the dependencies
+ if ((source.f & DIRTY) !== 0) {
+ execute_derived(/** @type {Derived} */ (source));
+ }
+ set_signal_status(source, (source.f & UNOWNED) === 0 ? CLEAN : MAYBE_DIRTY);
+ }
+
+ source.wv = increment_write_version();
+
mark_reactions(source, DIRTY);
// It's possible that the current reaction might not have up-to-date dependencies
diff --git a/packages/svelte/src/internal/client/reactivity/types.d.ts b/packages/svelte/src/internal/client/reactivity/types.d.ts
index 5ef0097649a4..90f922df686b 100644
--- a/packages/svelte/src/internal/client/reactivity/types.d.ts
+++ b/packages/svelte/src/internal/client/reactivity/types.d.ts
@@ -1,4 +1,11 @@
-import type { ComponentContext, Dom, Equals, TemplateNode, TransitionManager } from '#client';
+import type {
+ ComponentContext,
+ DevStackEntry,
+ Equals,
+ TemplateNode,
+ TransitionManager
+} from '#client';
+import type { Boundary } from '../dom/blocks/boundary';
export interface Signal {
/** Flags bitmask */
@@ -16,12 +23,21 @@ export interface Value extends Signal {
rv: number;
/** The latest value for this signal */
v: V;
- /** Dev only */
+
+ // dev-only
+ /** A label (e.g. the `foo` in `let foo = $state(...)`) used for `$inspect.trace()` */
+ label?: string;
+ /** An error with a stack trace showing when the source was created */
created?: Error | null;
+ /** An error with a stack trace showing when the source was last updated */
updated?: Error | null;
- trace_need_increase?: boolean;
- trace_v?: V;
- debug?: null | (() => void);
+ /**
+ * Whether or not the source was set while running an effect — if so, we need to
+ * increment the write version so that it shows up as dirty when the effect re-runs
+ */
+ set_during_effect?: boolean;
+ /** A function that retrieves the underlying source, used for each block item signals */
+ trace?: null | (() => void);
}
export interface Reaction extends Signal {
@@ -31,6 +47,8 @@ export interface Reaction extends Signal {
fn: null | Function;
/** Signals that this signal reads from */
deps: null | Value[];
+ /** An AbortController that aborts when the signal is destroyed */
+ ac: null | AbortController;
}
export interface Derived extends Value, Reaction {
@@ -67,8 +85,12 @@ export interface Effect extends Reaction {
last: null | Effect;
/** Parent effect */
parent: Effect | null;
+ /** The boundary this effect belongs to */
+ b: Boundary | null;
/** Dev only */
component_function?: any;
+ /** Dev only. Only set for certain block effects. Contains a reference to the stack that represents the render tree */
+ dev_stack?: DevStackEntry | null;
}
export type Source = Value;
diff --git a/packages/svelte/src/internal/client/render.js b/packages/svelte/src/internal/client/render.js
index 3256fe827410..ff6844453dcc 100644
--- a/packages/svelte/src/internal/client/render.js
+++ b/packages/svelte/src/internal/client/render.js
@@ -30,6 +30,7 @@ import * as w from './warnings.js';
import * as e from './errors.js';
import { assign_nodes } from './dom/template.js';
import { is_passive_event } from '../../utils.js';
+import { COMMENT_NODE } from './constants.js';
/**
* This is normally true — block effects should run their intro transitions —
@@ -107,7 +108,7 @@ export function hydrate(component, options) {
var anchor = /** @type {TemplateNode} */ (get_first_child(target));
while (
anchor &&
- (anchor.nodeType !== 8 || /** @type {Comment} */ (anchor).data !== HYDRATION_START)
+ (anchor.nodeType !== COMMENT_NODE || /** @type {Comment} */ (anchor).data !== HYDRATION_START)
) {
anchor = /** @type {TemplateNode} */ (get_next_sibling(anchor));
}
@@ -124,7 +125,7 @@ export function hydrate(component, options) {
if (
hydrate_node === null ||
- hydrate_node.nodeType !== 8 ||
+ hydrate_node.nodeType !== COMMENT_NODE ||
/** @type {Comment} */ (hydrate_node).data !== HYDRATION_END
) {
w.hydration_mismatch();
diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js
index 9f721f9ec4d6..fce6c78b56e4 100644
--- a/packages/svelte/src/internal/client/runtime.js
+++ b/packages/svelte/src/internal/client/runtime.js
@@ -1,4 +1,4 @@
-/** @import { ComponentContext, Derived, Effect, Reaction, Signal, Source, Value } from '#client' */
+/** @import { Derived, Effect, Reaction, Signal, Source, Value } from '#client' */
import { DEV } from 'esm-env';
import { define_property, get_descriptors, get_prototype_of, index_of } from '../shared/utils.js';
import {
@@ -20,30 +20,27 @@ import {
STATE_SYMBOL,
BLOCK_EFFECT,
ROOT_EFFECT,
- LEGACY_DERIVED_PROP,
DISCONNECTED,
- BOUNDARY_EFFECT
+ EFFECT_IS_UPDATING,
+ STALE_REACTION
} from './constants.js';
import { flush_tasks } from './dom/task.js';
-import { internal_set } from './reactivity/sources.js';
+import { internal_set, old_values } from './reactivity/sources.js';
import { destroy_derived_effects, update_derived } from './reactivity/deriveds.js';
import * as e from './errors.js';
-import { FILENAME } from '../../constants.js';
+
import { tracing_mode_flag } from '../flags/index.js';
import { tracing_expressions, get_stack } from './dev/tracing.js';
import {
component_context,
dev_current_component_function,
+ dev_stack,
is_runes,
set_component_context,
- set_dev_current_component_function
+ set_dev_current_component_function,
+ set_dev_stack
} from './context.js';
-import { is_firefox } from './dom/operations.js';
-
-// Used for DEV time error handling
-/** @param {WeakSet} value */
-const handled_errors = new WeakSet();
-let is_throwing_error = false;
+import { handle_error, invoke_error_boundary } from './error-handling.js';
let is_flushing = false;
@@ -87,17 +84,21 @@ export function set_active_effect(effect) {
}
/**
- * When sources are created within a derived, we record them so that we can safely allow
- * local mutations to these sources without the side-effect error being invoked unnecessarily.
- * @type {null | Source[]}
+ * When sources are created within a reaction, reading and writing
+ * them within that reaction should not cause a re-run
+ * @type {null | { reaction: Reaction, sources: Source[] }}
*/
-export let derived_sources = null;
+export let source_ownership = null;
-/**
- * @param {Source[] | null} sources
- */
-export function set_derived_sources(sources) {
- derived_sources = sources;
+/** @param {Value} value */
+export function push_reaction_value(value) {
+ if (active_reaction !== null && active_reaction.f & EFFECT_IS_UPDATING) {
+ if (source_ownership === null) {
+ source_ownership = { reaction: active_reaction, sources: [value] };
+ } else {
+ source_ownership.sources.push(value);
+ }
+ }
}
/**
@@ -222,140 +223,6 @@ export function check_dirtiness(reaction) {
return false;
}
-/**
- * @param {unknown} error
- * @param {Effect} effect
- */
-function propagate_error(error, effect) {
- /** @type {Effect | null} */
- var current = effect;
-
- while (current !== null) {
- if ((current.f & BOUNDARY_EFFECT) !== 0) {
- try {
- // @ts-expect-error
- current.fn(error);
- return;
- } catch {
- // Remove boundary flag from effect
- current.f ^= BOUNDARY_EFFECT;
- }
- }
-
- current = current.parent;
- }
-
- is_throwing_error = false;
- throw error;
-}
-
-/**
- * @param {Effect} effect
- */
-function should_rethrow_error(effect) {
- return (
- (effect.f & DESTROYED) === 0 &&
- (effect.parent === null || (effect.parent.f & BOUNDARY_EFFECT) === 0)
- );
-}
-
-export function reset_is_throwing_error() {
- is_throwing_error = false;
-}
-
-/**
- * @param {unknown} error
- * @param {Effect} effect
- * @param {Effect | null} previous_effect
- * @param {ComponentContext | null} component_context
- */
-export function handle_error(error, effect, previous_effect, component_context) {
- if (is_throwing_error) {
- if (previous_effect === null) {
- is_throwing_error = false;
- }
-
- if (should_rethrow_error(effect)) {
- throw error;
- }
-
- return;
- }
-
- if (previous_effect !== null) {
- is_throwing_error = true;
- }
-
- if (
- !DEV ||
- component_context === null ||
- !(error instanceof Error) ||
- handled_errors.has(error)
- ) {
- propagate_error(error, effect);
- return;
- }
-
- handled_errors.add(error);
-
- const component_stack = [];
-
- const effect_name = effect.fn?.name;
-
- if (effect_name) {
- component_stack.push(effect_name);
- }
-
- /** @type {ComponentContext | null} */
- let current_context = component_context;
-
- while (current_context !== null) {
- if (DEV) {
- /** @type {string} */
- var filename = current_context.function?.[FILENAME];
-
- if (filename) {
- const file = filename.split('/').pop();
- component_stack.push(file);
- }
- }
-
- current_context = current_context.p;
- }
-
- const indent = is_firefox ? ' ' : '\t';
- define_property(error, 'message', {
- value: error.message + `\n${component_stack.map((name) => `\n${indent}in ${name}`).join('')}\n`
- });
- define_property(error, 'component_stack', {
- value: component_stack
- });
-
- const stack = error.stack;
-
- // Filter out internal files from callstack
- if (stack) {
- const lines = stack.split('\n');
- const new_lines = [];
- for (let i = 0; i < lines.length; i++) {
- const line = lines[i];
- if (line.includes('svelte/src/internal')) {
- continue;
- }
- new_lines.push(line);
- }
- define_property(error, 'stack', {
- value: new_lines.join('\n')
- });
- }
-
- propagate_error(error, effect);
-
- if (should_rethrow_error(effect)) {
- throw error;
- }
-}
-
/**
* @param {Value} signal
* @param {Effect} effect
@@ -367,6 +234,14 @@ function schedule_possible_effect_self_invalidation(signal, effect, root = true)
for (var i = 0; i < reactions.length; i++) {
var reaction = reactions[i];
+
+ if (
+ source_ownership?.reaction === active_reaction &&
+ source_ownership.sources.includes(signal)
+ ) {
+ continue;
+ }
+
if ((reaction.f & DERIVED) !== 0) {
schedule_possible_effect_self_invalidation(/** @type {Derived} */ (reaction), effect, false);
} else if (effect === reaction) {
@@ -380,20 +255,17 @@ function schedule_possible_effect_self_invalidation(signal, effect, root = true)
}
}
-/**
- * @template V
- * @param {Reaction} reaction
- * @returns {V}
- */
+/** @param {Reaction} reaction */
export function update_reaction(reaction) {
var previous_deps = new_deps;
var previous_skipped_deps = skipped_deps;
var previous_untracked_writes = untracked_writes;
var previous_reaction = active_reaction;
var previous_skip_reaction = skip_reaction;
- var prev_derived_sources = derived_sources;
+ var previous_reaction_sources = source_ownership;
var previous_component_context = component_context;
var previous_untracking = untracking;
+
var flags = reaction.f;
new_deps = /** @type {null | Value[]} */ (null);
@@ -403,11 +275,18 @@ export function update_reaction(reaction) {
(flags & UNOWNED) !== 0 && (untracking || !is_updating_effect || active_reaction === null);
active_reaction = (flags & (BRANCH_EFFECT | ROOT_EFFECT)) === 0 ? reaction : null;
- derived_sources = null;
+ source_ownership = null;
set_component_context(reaction.ctx);
untracking = false;
read_version++;
+ reaction.f |= EFFECT_IS_UPDATING;
+
+ if (reaction.ac !== null) {
+ reaction.ac.abort(STALE_REACTION);
+ reaction.ac = null;
+ }
+
try {
var result = /** @type {Function} */ (0, reaction.fn)();
var deps = reaction.deps;
@@ -426,7 +305,12 @@ export function update_reaction(reaction) {
reaction.deps = deps = new_deps;
}
- if (!skip_reaction) {
+ if (
+ !skip_reaction ||
+ // Deriveds that already have reactions can cleanup, so we still add them as reactions
+ ((flags & DERIVED) !== 0 &&
+ /** @type {import('#client').Derived} */ (reaction).reactions !== null)
+ ) {
for (i = skipped_deps; i < deps.length; i++) {
(deps[i].reactions ??= []).push(reaction);
}
@@ -458,20 +342,32 @@ export function update_reaction(reaction) {
// we need to increment the read version to ensure that
// any dependencies in this reaction aren't marked with
// the same version
- if (previous_reaction !== null) {
+ if (previous_reaction !== null && previous_reaction !== reaction) {
read_version++;
+
+ if (untracked_writes !== null) {
+ if (previous_untracked_writes === null) {
+ previous_untracked_writes = untracked_writes;
+ } else {
+ previous_untracked_writes.push(.../** @type {Source[]} */ (untracked_writes));
+ }
+ }
}
return result;
+ } catch (error) {
+ handle_error(error);
} finally {
new_deps = previous_deps;
skipped_deps = previous_skipped_deps;
untracked_writes = previous_untracked_writes;
active_reaction = previous_reaction;
skip_reaction = previous_skip_reaction;
- derived_sources = prev_derived_sources;
+ source_ownership = previous_reaction_sources;
set_component_context(previous_component_context);
untracking = previous_untracking;
+
+ reaction.f ^= EFFECT_IS_UPDATING;
}
}
@@ -546,7 +442,6 @@ export function update_effect(effect) {
set_signal_status(effect, CLEAN);
var previous_effect = active_effect;
- var previous_component_context = component_context;
var was_updating_effect = is_updating_effect;
active_effect = effect;
@@ -555,6 +450,9 @@ export function update_effect(effect) {
if (DEV) {
var previous_component_fn = dev_current_component_function;
set_dev_current_component_function(effect.component_function);
+ var previous_stack = /** @type {any} */ (dev_stack);
+ // only block effects have a dev stack, keep the current one otherwise
+ set_dev_stack(effect.dev_stack ?? dev_stack);
}
try {
@@ -569,19 +467,13 @@ export function update_effect(effect) {
effect.teardown = typeof teardown === 'function' ? teardown : null;
effect.wv = write_version;
- var deps = effect.deps;
-
- // In DEV, we need to handle a case where $inspect.trace() might
- // incorrectly state a source dependency has not changed when it has.
- // That's beacuse that source was changed by the same effect, causing
- // the versions to match. We can avoid this by incrementing the version
- if (DEV && tracing_mode_flag && (effect.f & DIRTY) !== 0 && deps !== null) {
- for (let i = 0; i < deps.length; i++) {
- var dep = deps[i];
- if (dep.trace_need_increase) {
+ // In DEV, increment versions of any sources that were written to during the effect,
+ // so that they are correctly marked as dirty when the effect re-runs
+ if (DEV && tracing_mode_flag && (effect.f & DIRTY) !== 0 && effect.deps !== null) {
+ for (var dep of effect.deps) {
+ if (dep.set_during_effect) {
dep.wv = increment_write_version();
- dep.trace_need_increase = undefined;
- dep.trace_v = undefined;
+ dep.set_during_effect = false;
}
}
}
@@ -589,14 +481,13 @@ export function update_effect(effect) {
if (DEV) {
dev_effect_stack.push(effect);
}
- } catch (error) {
- handle_error(error, effect, previous_effect, previous_component_context || effect.ctx);
} finally {
is_updating_effect = was_updating_effect;
active_effect = previous_effect;
if (DEV) {
set_dev_current_component_function(previous_component_fn);
+ set_dev_stack(previous_stack);
}
}
}
@@ -625,14 +516,14 @@ function infinite_loop_guard() {
if (last_scheduled_effect !== null) {
if (DEV) {
try {
- handle_error(error, last_scheduled_effect, null, null);
+ invoke_error_boundary(error, last_scheduled_effect);
} catch (e) {
// Only log the effect stack if the error is re-thrown
log_effect_stack();
throw e;
}
} else {
- handle_error(error, last_scheduled_effect, null, null);
+ invoke_error_boundary(error, last_scheduled_effect);
}
} else {
if (DEV) {
@@ -661,15 +552,10 @@ function flush_queued_root_effects() {
queued_root_effects = [];
for (var i = 0; i < length; i++) {
- var root = root_effects[i];
-
- if ((root.f & CLEAN) === 0) {
- root.f ^= CLEAN;
- }
-
- var collected_effects = process_effects(root);
+ var collected_effects = process_effects(root_effects[i]);
flush_queued_effects(collected_effects);
}
+ old_values.clear();
}
} finally {
is_flushing = false;
@@ -694,27 +580,23 @@ function flush_queued_effects(effects) {
var effect = effects[i];
if ((effect.f & (DESTROYED | INERT)) === 0) {
- try {
- if (check_dirtiness(effect)) {
- update_effect(effect);
-
- // Effects with no dependencies or teardown do not get added to the effect tree.
- // Deferred effects (e.g. `$effect(...)`) _are_ added to the tree because we
- // don't know if we need to keep them until they are executed. Doing the check
- // here (rather than in `update_effect`) allows us to skip the work for
- // immediate effects.
- if (effect.deps === null && effect.first === null && effect.nodes_start === null) {
- if (effect.teardown === null) {
- // remove this effect from the graph
- unlink_effect(effect);
- } else {
- // keep the effect in the graph, but free up some memory
- effect.fn = null;
- }
+ if (check_dirtiness(effect)) {
+ update_effect(effect);
+
+ // Effects with no dependencies or teardown do not get added to the effect tree.
+ // Deferred effects (e.g. `$effect(...)`) _are_ added to the tree because we
+ // don't know if we need to keep them until they are executed. Doing the check
+ // here (rather than in `update_effect`) allows us to skip the work for
+ // immediate effects.
+ if (effect.deps === null && effect.first === null && effect.nodes_start === null) {
+ if (effect.teardown === null) {
+ // remove this effect from the graph
+ unlink_effect(effect);
+ } else {
+ // keep the effect in the graph, but free up some memory
+ effect.fn = null;
}
}
- } catch (error) {
- handle_error(error, effect, null, effect.ctx);
}
}
}
@@ -759,11 +641,12 @@ function process_effects(root) {
/** @type {Effect[]} */
var effects = [];
- var effect = root.first;
+ /** @type {Effect | null} */
+ var effect = root;
while (effect !== null) {
var flags = effect.f;
- var is_branch = (flags & BRANCH_EFFECT) !== 0;
+ var is_branch = (flags & (BRANCH_EFFECT | ROOT_EFFECT)) !== 0;
var is_skippable_branch = is_branch && (flags & CLEAN) !== 0;
if (!is_skippable_branch && (flags & INERT) === 0) {
@@ -772,22 +655,12 @@ function process_effects(root) {
} else if (is_branch) {
effect.f ^= CLEAN;
} else {
- // Ensure we set the effect to be the active reaction
- // to ensure that unowned deriveds are correctly tracked
- // because we're flushing the current effect
- var previous_active_reaction = active_reaction;
- try {
- active_reaction = effect;
- if (check_dirtiness(effect)) {
- update_effect(effect);
- }
- } catch (error) {
- handle_error(error, effect, null, effect.ctx);
- } finally {
- active_reaction = previous_active_reaction;
+ if (check_dirtiness(effect)) {
+ update_effect(effect);
}
}
+ /** @type {Effect | null} */
var child = effect.first;
if (child !== null) {
@@ -821,18 +694,28 @@ export function flushSync(fn) {
if (fn) {
is_flushing = true;
flush_queued_root_effects();
+
+ is_flushing = true;
result = fn();
}
- flush_tasks();
+ while (true) {
+ flush_tasks();
+
+ if (queued_root_effects.length === 0) {
+ // this would be reset in `flush_queued_root_effects` but since we are early returning here,
+ // we need to reset it here as well in case the first time there's 0 queued root effects
+ is_flushing = false;
+ last_scheduled_effect = null;
+ if (DEV) {
+ dev_effect_stack = [];
+ }
+ return /** @type {T} */ (result);
+ }
- while (queued_root_effects.length > 0) {
is_flushing = true;
flush_queued_root_effects();
- flush_tasks();
}
-
- return /** @type {T} */ (result);
}
/**
@@ -861,24 +744,26 @@ export function get(signal) {
// Register the dependency on the current reaction signal.
if (active_reaction !== null && !untracking) {
- if (derived_sources !== null && derived_sources.includes(signal)) {
- e.state_unsafe_local_read();
- }
- var deps = active_reaction.deps;
- if (signal.rv < read_version) {
- signal.rv = read_version;
- // If the signal is accessing the same dependencies in the same
- // order as it did last time, increment `skipped_deps`
- // rather than updating `new_deps`, which creates GC cost
- if (new_deps === null && deps !== null && deps[skipped_deps] === signal) {
- skipped_deps++;
- } else if (new_deps === null) {
- new_deps = [signal];
- } else if (!skip_reaction || !new_deps.includes(signal)) {
- // Normally we can push duplicated dependencies to `new_deps`, but if we're inside
- // an unowned derived because skip_reaction is true, then we need to ensure that
- // we don't have duplicates
- new_deps.push(signal);
+ if (
+ source_ownership?.reaction !== active_reaction ||
+ !source_ownership?.sources.includes(signal)
+ ) {
+ var deps = active_reaction.deps;
+ if (signal.rv < read_version) {
+ signal.rv = read_version;
+ // If the signal is accessing the same dependencies in the same
+ // order as it did last time, increment `skipped_deps`
+ // rather than updating `new_deps`, which creates GC cost
+ if (new_deps === null && deps !== null && deps[skipped_deps] === signal) {
+ skipped_deps++;
+ } else if (new_deps === null) {
+ new_deps = [signal];
+ } else if (!skip_reaction || !new_deps.includes(signal)) {
+ // Normally we can push duplicated dependencies to `new_deps`, but if we're inside
+ // an unowned derived because skip_reaction is true, then we need to ensure that
+ // we don't have duplicates
+ new_deps.push(signal);
+ }
}
}
} else if (
@@ -908,25 +793,40 @@ export function get(signal) {
if (
DEV &&
tracing_mode_flag &&
+ !untracking &&
tracing_expressions !== null &&
active_reaction !== null &&
tracing_expressions.reaction === active_reaction
) {
// Used when mapping state between special blocks like `each`
- if (signal.debug) {
- signal.debug();
- } else if (signal.created) {
- var entry = tracing_expressions.entries.get(signal);
-
- if (entry === undefined) {
- entry = { read: [] };
- tracing_expressions.entries.set(signal, entry);
- }
+ if (signal.trace) {
+ signal.trace();
+ } else {
+ var trace = get_stack('TracedAt');
+
+ if (trace) {
+ var entry = tracing_expressions.entries.get(signal);
+
+ if (entry === undefined) {
+ entry = { traces: [] };
+ tracing_expressions.entries.set(signal, entry);
+ }
+
+ var last = entry.traces[entry.traces.length - 1];
- entry.read.push(get_stack('TracedAt'));
+ // traces can be duplicated, e.g. by `snapshot` invoking both
+ // both `getOwnPropertyDescriptor` and `get` traps at once
+ if (trace.stack !== last?.stack) {
+ entry.traces.push(trace);
+ }
+ }
}
}
+ if (is_destroying_effect && old_values.has(signal)) {
+ return old_values.get(signal);
+ }
+
return signal.v;
}
@@ -975,17 +875,7 @@ export function invalidate_inner_signals(fn) {
var captured = capture_signals(() => untrack(fn));
for (var signal of captured) {
- // Go one level up because derived signals created as part of props in legacy mode
- if ((signal.f & LEGACY_DERIVED_PROP) !== 0) {
- for (const dep of /** @type {Derived} */ (signal).deps || []) {
- if ((dep.f & DERIVED) === 0) {
- // Use internal_set instead of set here and below to avoid mutation validation
- internal_set(dep, dep.v);
- }
- }
- } else {
- internal_set(signal, signal.v);
- }
+ internal_set(signal, signal.v);
}
}
diff --git a/packages/svelte/src/internal/client/types.d.ts b/packages/svelte/src/internal/client/types.d.ts
index 7208ed77837e..0b7310e1726b 100644
--- a/packages/svelte/src/internal/client/types.d.ts
+++ b/packages/svelte/src/internal/client/types.d.ts
@@ -14,6 +14,8 @@ export type ComponentContext = {
p: null | ComponentContext;
/** context */
c: null | Map;
+ /** destroyed */
+ d: boolean;
/** deferred effects */
e: null | Array<{
fn: () => void | (() => void);
@@ -177,16 +179,21 @@ export type TaskCallback = (now: number) => boolean | void;
export type TaskEntry = { c: TaskCallback; f: () => void };
-/** Dev-only */
-export interface ProxyMetadata {
- /** The components that 'own' this state, if any. `null` means no owners, i.e. everyone can mutate this state. */
- owners: null | Set;
- /** The parent metadata object */
- parent: null | ProxyMetadata;
-}
-
export type ProxyStateObject> = T & {
[STATE_SYMBOL]: T;
};
+export type SourceLocation =
+ | [line: number, column: number]
+ | [line: number, column: number, SourceLocation[]];
+
+export interface DevStackEntry {
+ file: string;
+ type: 'component' | 'if' | 'each' | 'await' | 'key' | 'render';
+ line: number;
+ column: number;
+ parent: DevStackEntry | null;
+ componentTag?: string;
+}
+
export * from './reactivity/types';
diff --git a/packages/svelte/src/internal/client/warnings.js b/packages/svelte/src/internal/client/warnings.js
index 250c6eca2fe9..74f9041b91dd 100644
--- a/packages/svelte/src/internal/client/warnings.js
+++ b/packages/svelte/src/internal/client/warnings.js
@@ -25,7 +25,13 @@ export function assignment_value_stale(property, location) {
*/
export function binding_property_non_reactive(binding, location) {
if (DEV) {
- console.warn(`%c[svelte] binding_property_non_reactive\n%c${location ? `\`${binding}\` (${location}) is binding to a non-reactive property` : `\`${binding}\` is binding to a non-reactive property`}\nhttps://svelte.dev/e/binding_property_non_reactive`, bold, normal);
+ console.warn(
+ `%c[svelte] binding_property_non_reactive\n%c${location
+ ? `\`${binding}\` (${location}) is binding to a non-reactive property`
+ : `\`${binding}\` is binding to a non-reactive property`}\nhttps://svelte.dev/e/binding_property_non_reactive`,
+ bold,
+ normal
+ );
} else {
console.warn(`https://svelte.dev/e/binding_property_non_reactive`);
}
@@ -76,7 +82,13 @@ export function hydration_attribute_changed(attribute, html, value) {
*/
export function hydration_html_changed(location) {
if (DEV) {
- console.warn(`%c[svelte] hydration_html_changed\n%c${location ? `The value of an \`{@html ...}\` block ${location} changed between server and client renders. The client value will be ignored in favour of the server value` : 'The value of an `{@html ...}` block changed between server and client renders. The client value will be ignored in favour of the server value'}\nhttps://svelte.dev/e/hydration_html_changed`, bold, normal);
+ console.warn(
+ `%c[svelte] hydration_html_changed\n%c${location
+ ? `The value of an \`{@html ...}\` block ${location} changed between server and client renders. The client value will be ignored in favour of the server value`
+ : 'The value of an `{@html ...}` block changed between server and client renders. The client value will be ignored in favour of the server value'}\nhttps://svelte.dev/e/hydration_html_changed`,
+ bold,
+ normal
+ );
} else {
console.warn(`https://svelte.dev/e/hydration_html_changed`);
}
@@ -88,7 +100,13 @@ export function hydration_html_changed(location) {
*/
export function hydration_mismatch(location) {
if (DEV) {
- console.warn(`%c[svelte] hydration_mismatch\n%c${location ? `Hydration failed because the initial UI does not match what was rendered on the server. The error occurred near ${location}` : 'Hydration failed because the initial UI does not match what was rendered on the server'}\nhttps://svelte.dev/e/hydration_mismatch`, bold, normal);
+ console.warn(
+ `%c[svelte] hydration_mismatch\n%c${location
+ ? `Hydration failed because the initial UI does not match what was rendered on the server. The error occurred near ${location}`
+ : 'Hydration failed because the initial UI does not match what was rendered on the server'}\nhttps://svelte.dev/e/hydration_mismatch`,
+ bold,
+ normal
+ );
} else {
console.warn(`https://svelte.dev/e/hydration_mismatch`);
}
@@ -129,32 +147,46 @@ export function lifecycle_double_unmount() {
}
/**
- * %parent% passed a value to %child% with `bind:`, but the value is owned by %owner%. Consider creating a binding between %owner% and %parent%
+ * %parent% passed property `%prop%` to %child% with `bind:`, but its parent component %owner% did not declare `%prop%` as a binding. Consider creating a binding between %owner% and %parent% (e.g. `bind:%prop%={...}` instead of `%prop%={...}`)
* @param {string} parent
+ * @param {string} prop
* @param {string} child
* @param {string} owner
*/
-export function ownership_invalid_binding(parent, child, owner) {
+export function ownership_invalid_binding(parent, prop, child, owner) {
if (DEV) {
- console.warn(`%c[svelte] ownership_invalid_binding\n%c${parent} passed a value to ${child} with \`bind:\`, but the value is owned by ${owner}. Consider creating a binding between ${owner} and ${parent}\nhttps://svelte.dev/e/ownership_invalid_binding`, bold, normal);
+ console.warn(`%c[svelte] ownership_invalid_binding\n%c${parent} passed property \`${prop}\` to ${child} with \`bind:\`, but its parent component ${owner} did not declare \`${prop}\` as a binding. Consider creating a binding between ${owner} and ${parent} (e.g. \`bind:${prop}={...}\` instead of \`${prop}={...}\`)\nhttps://svelte.dev/e/ownership_invalid_binding`, bold, normal);
} else {
console.warn(`https://svelte.dev/e/ownership_invalid_binding`);
}
}
/**
- * %component% mutated a value owned by %owner%. This is strongly discouraged. Consider passing values to child components with `bind:`, or use a callback instead
- * @param {string | undefined | null} [component]
- * @param {string | undefined | null} [owner]
+ * Mutating unbound props (`%name%`, at %location%) is strongly discouraged. Consider using `bind:%prop%={...}` in %parent% (or using a callback) instead
+ * @param {string} name
+ * @param {string} location
+ * @param {string} prop
+ * @param {string} parent
*/
-export function ownership_invalid_mutation(component, owner) {
+export function ownership_invalid_mutation(name, location, prop, parent) {
if (DEV) {
- console.warn(`%c[svelte] ownership_invalid_mutation\n%c${component ? `${component} mutated a value owned by ${owner}. This is strongly discouraged. Consider passing values to child components with \`bind:\`, or use a callback instead` : 'Mutating a value outside the component that created it is strongly discouraged. Consider passing values to child components with `bind:`, or use a callback instead'}\nhttps://svelte.dev/e/ownership_invalid_mutation`, bold, normal);
+ console.warn(`%c[svelte] ownership_invalid_mutation\n%cMutating unbound props (\`${name}\`, at ${location}) is strongly discouraged. Consider using \`bind:${prop}={...}\` in ${parent} (or using a callback) instead\nhttps://svelte.dev/e/ownership_invalid_mutation`, bold, normal);
} else {
console.warn(`https://svelte.dev/e/ownership_invalid_mutation`);
}
}
+/**
+ * The `value` property of a `` element should be an array, but it received a non-array value. The selection will be kept as is.
+ */
+export function select_multiple_invalid_value() {
+ if (DEV) {
+ console.warn(`%c[svelte] select_multiple_invalid_value\n%cThe \`value\` property of a \`\` element should be an array, but it received a non-array value. The selection will be kept as is.\nhttps://svelte.dev/e/select_multiple_invalid_value`, bold, normal);
+ } else {
+ console.warn(`https://svelte.dev/e/select_multiple_invalid_value`);
+ }
+}
+
/**
* Reactive `$state(...)` proxies and the values they proxy have different identities. Because of this, comparisons with `%operator%` will produce unexpected results
* @param {string} operator
diff --git a/packages/svelte/src/internal/server/abort-signal.js b/packages/svelte/src/internal/server/abort-signal.js
new file mode 100644
index 000000000000..da579b25927b
--- /dev/null
+++ b/packages/svelte/src/internal/server/abort-signal.js
@@ -0,0 +1,13 @@
+import { STALE_REACTION } from '#client/constants';
+
+/** @type {AbortController | null} */
+export let controller = null;
+
+export function abort() {
+ controller?.abort(STALE_REACTION);
+ controller = null;
+}
+
+export function getAbortSignal() {
+ return (controller ??= new AbortController()).signal;
+}
diff --git a/packages/svelte/src/internal/server/blocks/snippet.js b/packages/svelte/src/internal/server/blocks/snippet.js
index 3c5e8607905b..9e96ae34305d 100644
--- a/packages/svelte/src/internal/server/blocks/snippet.js
+++ b/packages/svelte/src/internal/server/blocks/snippet.js
@@ -1,5 +1,5 @@
/** @import { Snippet } from 'svelte' */
-/** @import { Payload } from '#server' */
+/** @import { Payload } from '../payload' */
/** @import { Getters } from '#shared' */
/**
diff --git a/packages/svelte/src/internal/server/dev.js b/packages/svelte/src/internal/server/dev.js
index ecf4e67429ac..efc761d7c5ef 100644
--- a/packages/svelte/src/internal/server/dev.js
+++ b/packages/svelte/src/internal/server/dev.js
@@ -1,10 +1,12 @@
-/** @import { Component, Payload } from '#server' */
+/** @import { Component } from '#server' */
import { FILENAME } from '../../constants.js';
import {
is_tag_valid_with_ancestor,
is_tag_valid_with_parent
} from '../../html-tree-validation.js';
import { current_component } from './context.js';
+import { invalid_snippet_arguments } from '../shared/errors.js';
+import { HeadPayload, Payload } from './payload.js';
/**
* @typedef {{
@@ -24,14 +26,6 @@ let parent = null;
/** @type {Set} */
let seen;
-/**
- * @param {Element} element
- */
-function stringify(element) {
- if (element.filename === null) return `\`<${element.tag}>\``;
- return `\`<${element.tag}>\` (${element.filename}:${element.line}:${element.column})`;
-}
-
/**
* @param {Payload} payload
* @param {string} message
@@ -98,3 +92,16 @@ export function push_element(payload, tag, line, column) {
export function pop_element() {
parent = /** @type {Element} */ (parent).parent;
}
+
+/**
+ * @param {Payload} payload
+ */
+export function validate_snippet_args(payload) {
+ if (
+ typeof payload !== 'object' ||
+ // for some reason typescript consider the type of payload as never after the first instanceof
+ !(payload instanceof Payload || /** @type {any} */ (payload) instanceof HeadPayload)
+ ) {
+ invalid_snippet_arguments();
+ }
+}
diff --git a/packages/svelte/src/internal/server/errors.js b/packages/svelte/src/internal/server/errors.js
index 38c545c84ec8..e47530c9aaf9 100644
--- a/packages/svelte/src/internal/server/errors.js
+++ b/packages/svelte/src/internal/server/errors.js
@@ -1,5 +1,7 @@
/* This file is generated by scripts/process-messages/index.js. Do not edit! */
+
+
/**
* `%name%(...)` is not available on the server
* @param {string} name
@@ -9,5 +11,6 @@ export function lifecycle_function_unavailable(name) {
const error = new Error(`lifecycle_function_unavailable\n\`${name}(...)\` is not available on the server\nhttps://svelte.dev/e/lifecycle_function_unavailable`);
error.name = 'Svelte error';
+
throw error;
}
\ No newline at end of file
diff --git a/packages/svelte/src/internal/server/index.js b/packages/svelte/src/internal/server/index.js
index 2591dbe4eaab..ceb516ebb0db 100644
--- a/packages/svelte/src/internal/server/index.js
+++ b/packages/svelte/src/internal/server/index.js
@@ -1,8 +1,8 @@
/** @import { ComponentType, SvelteComponent } from 'svelte' */
-/** @import { Component, Payload, RenderOutput } from '#server' */
+/** @import { Component, RenderOutput } from '#server' */
/** @import { Store } from '#shared' */
export { FILENAME, HMR } from '../../constants.js';
-import { attr, clsx, to_class } from '../shared/attributes.js';
+import { attr, clsx, to_class, to_style } from '../shared/attributes.js';
import { is_promise, noop } from '../shared/utils.js';
import { subscribe_to_store } from '../../store/utils.js';
import {
@@ -13,46 +13,18 @@ import {
import { escape_html } from '../../escaping.js';
import { DEV } from 'esm-env';
import { current_component, pop, push } from './context.js';
-import { EMPTY_COMMENT, BLOCK_CLOSE, BLOCK_OPEN } from './hydration.js';
+import { EMPTY_COMMENT, BLOCK_CLOSE, BLOCK_OPEN, BLOCK_OPEN_ELSE } from './hydration.js';
import { validate_store } from '../shared/validate.js';
import { is_boolean_attribute, is_raw_text_element, is_void } from '../../utils.js';
import { reset_elements } from './dev.js';
+import { Payload } from './payload.js';
+import { abort } from './abort-signal.js';
// https://html.spec.whatwg.org/multipage/syntax.html#attributes-2
// https://infra.spec.whatwg.org/#noncharacter
const INVALID_ATTR_NAME_CHAR_REGEX =
/[\s'">/=\u{FDD0}-\u{FDEF}\u{FFFE}\u{FFFF}\u{1FFFE}\u{1FFFF}\u{2FFFE}\u{2FFFF}\u{3FFFE}\u{3FFFF}\u{4FFFE}\u{4FFFF}\u{5FFFE}\u{5FFFF}\u{6FFFE}\u{6FFFF}\u{7FFFE}\u{7FFFF}\u{8FFFE}\u{8FFFF}\u{9FFFE}\u{9FFFF}\u{AFFFE}\u{AFFFF}\u{BFFFE}\u{BFFFF}\u{CFFFE}\u{CFFFF}\u{DFFFE}\u{DFFFF}\u{EFFFE}\u{EFFFF}\u{FFFFE}\u{FFFFF}\u{10FFFE}\u{10FFFF}]/u;
-/**
- * @param {Payload} to_copy
- * @returns {Payload}
- */
-export function copy_payload({ out, css, head, uid }) {
- return {
- out,
- css: new Set(css),
- head: {
- title: head.title,
- out: head.out,
- css: new Set(head.css),
- uid: head.uid
- },
- uid
- };
-}
-
-/**
- * Assigns second payload to first
- * @param {Payload} p1
- * @param {Payload} p2
- * @returns {void}
- */
-export function assign_payload(p1, p2) {
- p1.out = p2.out;
- p1.head = p2.head;
- p1.uid = p2.uid;
-}
-
/**
* @param {Payload} payload
* @param {string} tag
@@ -86,16 +58,6 @@ export function element(payload, tag, attributes_fn = noop, children_fn = noop)
*/
export let on_destroy = [];
-/**
- * Creates an ID generator
- * @param {string} prefix
- * @returns {() => string}
- */
-function props_id_generator(prefix) {
- let uid = 1;
- return () => `${prefix}s${uid++}`;
-}
-
/**
* Only available on the server and when compiling with the `server` option.
* Takes a component and returns an object with `body` and `head` properties on it, which you can use to populate the HTML when server-rendering your app.
@@ -105,57 +67,54 @@ function props_id_generator(prefix) {
* @returns {RenderOutput}
*/
export function render(component, options = {}) {
- const uid = props_id_generator(options.idPrefix ? options.idPrefix + '-' : '');
- /** @type {Payload} */
- const payload = {
- out: '',
- css: new Set(),
- head: { title: '', out: '', css: new Set(), uid },
- uid
- };
+ try {
+ const payload = new Payload(options.idPrefix ? options.idPrefix + '-' : '');
- const prev_on_destroy = on_destroy;
- on_destroy = [];
- payload.out += BLOCK_OPEN;
+ const prev_on_destroy = on_destroy;
+ on_destroy = [];
+ payload.out += BLOCK_OPEN;
- let reset_reset_element;
+ let reset_reset_element;
- if (DEV) {
- // prevent parent/child element state being corrupted by a bad render
- reset_reset_element = reset_elements();
- }
+ if (DEV) {
+ // prevent parent/child element state being corrupted by a bad render
+ reset_reset_element = reset_elements();
+ }
- if (options.context) {
- push();
- /** @type {Component} */ (current_component).c = options.context;
- }
+ if (options.context) {
+ push();
+ /** @type {Component} */ (current_component).c = options.context;
+ }
- // @ts-expect-error
- component(payload, options.props ?? {}, {}, {});
+ // @ts-expect-error
+ component(payload, options.props ?? {}, {}, {});
- if (options.context) {
- pop();
- }
+ if (options.context) {
+ pop();
+ }
- if (reset_reset_element) {
- reset_reset_element();
- }
+ if (reset_reset_element) {
+ reset_reset_element();
+ }
- payload.out += BLOCK_CLOSE;
- for (const cleanup of on_destroy) cleanup();
- on_destroy = prev_on_destroy;
+ payload.out += BLOCK_CLOSE;
+ for (const cleanup of on_destroy) cleanup();
+ on_destroy = prev_on_destroy;
- let head = payload.head.out + payload.head.title;
+ let head = payload.head.out + payload.head.title;
- for (const { hash, code } of payload.css) {
- head += ``;
- }
+ for (const { hash, code } of payload.css) {
+ head += ``;
+ }
- return {
- head,
- html: payload.out,
- body: payload.out
- };
+ return {
+ head,
+ html: payload.out,
+ body: payload.out
+ };
+ } finally {
+ abort();
+ }
}
/**
@@ -210,9 +169,7 @@ export function css_props(payload, is_html, props, component, dynamic = false) {
*/
export function spread_attributes(attrs, css_hash, classes, styles, flags = 0) {
if (styles) {
- attrs.style = attrs.style
- ? style_object_to_string(merge_styles(/** @type {string} */ (attrs.style), styles))
- : style_object_to_string(styles);
+ attrs.style = to_style(attrs.style, styles);
}
if (attrs.class) {
@@ -286,35 +243,23 @@ function style_object_to_string(style_object) {
.join(' ');
}
-/** @param {Record} style_object */
-export function add_styles(style_object) {
- const styles = style_object_to_string(style_object);
- return styles ? ` style="${styles}"` : '';
+/**
+ * @param {any} value
+ * @param {string | undefined} [hash]
+ * @param {Record} [directives]
+ */
+export function attr_class(value, hash, directives) {
+ var result = to_class(value, hash, directives);
+ return result ? ` class="${escape_html(result, true)}"` : '';
}
/**
- * @param {string} attribute
- * @param {Record} styles
+ * @param {any} value
+ * @param {Record|[Record,Record]} [directives]
*/
-export function merge_styles(attribute, styles) {
- /** @type {Record} */
- var merged = {};
-
- if (attribute) {
- for (var declaration of attribute.split(';')) {
- var i = declaration.indexOf(':');
- var name = declaration.slice(0, i).trim();
- var value = declaration.slice(i + 1).trim();
-
- if (name !== '') merged[name] = value;
- }
- }
-
- for (name in styles) {
- merged[name] = styles[name];
- }
-
- return merged;
+export function attr_style(value, directives) {
+ var result = to_style(value, directives);
+ return result ? ` style="${escape_html(result, true)}"` : '';
}
/**
@@ -487,18 +432,21 @@ export function bind_props(props_parent, props_now) {
/**
* @template V
+ * @param {Payload} payload
* @param {Promise} promise
* @param {null | (() => void)} pending_fn
* @param {(value: V) => void} then_fn
* @returns {void}
*/
-function await_block(promise, pending_fn, then_fn) {
+function await_block(payload, promise, pending_fn, then_fn) {
if (is_promise(promise)) {
+ payload.out += BLOCK_OPEN;
promise.then(null, noop);
if (pending_fn !== null) {
pending_fn();
}
} else if (then_fn !== null) {
+ payload.out += BLOCK_OPEN_ELSE;
then_fn(promise);
}
}
@@ -549,22 +497,73 @@ export function props_id(payload) {
return uid;
}
-export { attr, clsx, to_class };
+export { attr, clsx };
export { html } from './blocks/html.js';
export { push, pop } from './context.js';
-export { push_element, pop_element } from './dev.js';
+export { push_element, pop_element, validate_snippet_args } from './dev.js';
+
+export { assign_payload, copy_payload } from './payload.js';
export { snapshot } from '../shared/clone.js';
-export { fallback } from '../shared/utils.js';
+export { fallback, to_array } from '../shared/utils.js';
export {
invalid_default_snippet,
validate_dynamic_element_tag,
- validate_void_dynamic_element
+ validate_void_dynamic_element,
+ prevent_snippet_stringification
} from '../shared/validate.js';
export { escape_html as escape };
+
+/**
+ * @template T
+ * @param {()=>T} fn
+ * @returns {(new_value?: T) => (T | void)}
+ */
+export function derived(fn) {
+ const get_value = once(fn);
+ /**
+ * @type {T | undefined}
+ */
+ let updated_value;
+
+ return function (new_value) {
+ if (arguments.length === 0) {
+ return updated_value ?? get_value();
+ }
+ updated_value = new_value;
+ return updated_value;
+ };
+}
+
+/**
+ *
+ * @param {Payload} payload
+ * @param {*} value
+ */
+export function maybe_selected(payload, value) {
+ return value === payload.select_value ? ' selected' : '';
+}
+
+/**
+ * @param {Payload} payload
+ * @param {() => void} children
+ * @returns {void}
+ */
+export function valueless_option(payload, children) {
+ var i = payload.out.length;
+
+ children();
+
+ var body = payload.out.slice(i);
+
+ if (body.replace(//g, '') === payload.select_value) {
+ // replace '>' with ' selected>' (closing tag will be added later)
+ payload.out = payload.out.slice(0, i - 1) + ' selected>' + body;
+ }
+}
diff --git a/packages/svelte/src/internal/server/payload.js b/packages/svelte/src/internal/server/payload.js
new file mode 100644
index 000000000000..a31120ae1686
--- /dev/null
+++ b/packages/svelte/src/internal/server/payload.js
@@ -0,0 +1,73 @@
+export class HeadPayload {
+ /** @type {Set<{ hash: string; code: string }>} */
+ css = new Set();
+ out = '';
+ uid = () => '';
+ title = '';
+
+ constructor(css = new Set(), out = '', title = '', uid = () => '') {
+ this.css = css;
+ this.out = out;
+ this.title = title;
+ this.uid = uid;
+ }
+}
+
+export class Payload {
+ /** @type {Set<{ hash: string; code: string }>} */
+ css = new Set();
+ out = '';
+ uid = () => '';
+ select_value = undefined;
+
+ head = new HeadPayload();
+
+ constructor(id_prefix = '') {
+ this.uid = props_id_generator(id_prefix);
+ this.head.uid = this.uid;
+ }
+}
+
+/**
+ * Used in legacy mode to handle bindings
+ * @param {Payload} to_copy
+ * @returns {Payload}
+ */
+export function copy_payload({ out, css, head, uid }) {
+ const payload = new Payload();
+
+ payload.out = out;
+ payload.css = new Set(css);
+ payload.uid = uid;
+
+ payload.head = new HeadPayload();
+ payload.head.out = head.out;
+ payload.head.css = new Set(head.css);
+ payload.head.title = head.title;
+ payload.head.uid = head.uid;
+
+ return payload;
+}
+
+/**
+ * Assigns second payload to first
+ * @param {Payload} p1
+ * @param {Payload} p2
+ * @returns {void}
+ */
+export function assign_payload(p1, p2) {
+ p1.out = p2.out;
+ p1.css = p2.css;
+ p1.head = p2.head;
+ p1.uid = p2.uid;
+}
+
+/**
+ * Creates an ID generator
+ * @param {string} prefix
+ * @returns {() => string}
+ */
+function props_id_generator(prefix) {
+ let uid = 1;
+ return () => `${prefix}s${uid++}`;
+}
diff --git a/packages/svelte/src/internal/server/types.d.ts b/packages/svelte/src/internal/server/types.d.ts
index 2fffdbbdf0bb..6b0fc146c419 100644
--- a/packages/svelte/src/internal/server/types.d.ts
+++ b/packages/svelte/src/internal/server/types.d.ts
@@ -11,19 +11,6 @@ export interface Component {
function?: any;
}
-export interface Payload {
- out: string;
- css: Set<{ hash: string; code: string }>;
- head: {
- title: string;
- out: string;
- uid: () => string;
- css: Set<{ hash: string; code: string }>;
- };
- /** Function that generates a unique ID */
- uid: () => string;
-}
-
export interface RenderOutput {
/** HTML that goes into the `` */
head: string;
diff --git a/packages/svelte/src/internal/shared/attributes.js b/packages/svelte/src/internal/shared/attributes.js
index 89cc17e51b9d..c8758c1d4d4d 100644
--- a/packages/svelte/src/internal/shared/attributes.js
+++ b/packages/svelte/src/internal/shared/attributes.js
@@ -22,7 +22,7 @@ const replacements = {
* @returns {string}
*/
export function attr(name, value, is_boolean = false) {
- if (value == null || (!value && is_boolean) || (value === '' && name === 'class')) return '';
+ if (value == null || (!value && is_boolean)) return '';
const normalized = (name in replacements && replacements[name].get(value)) || value;
const assignment = is_boolean ? '' : `="${escape_html(normalized, true)}"`;
return ` ${name}${assignment}`;
@@ -82,3 +82,138 @@ export function to_class(value, hash, directives) {
return classname === '' ? null : classname;
}
+
+/**
+ *
+ * @param {Record} styles
+ * @param {boolean} important
+ */
+function append_styles(styles, important = false) {
+ var separator = important ? ' !important;' : ';';
+ var css = '';
+
+ for (var key in styles) {
+ var value = styles[key];
+ if (value != null && value !== '') {
+ css += ' ' + key + ': ' + value + separator;
+ }
+ }
+
+ return css;
+}
+
+/**
+ * @param {string} name
+ * @returns {string}
+ */
+function to_css_name(name) {
+ if (name[0] !== '-' || name[1] !== '-') {
+ return name.toLowerCase();
+ }
+ return name;
+}
+
+/**
+ * @param {any} value
+ * @param {Record | [Record, Record]} [styles]
+ * @returns {string | null}
+ */
+export function to_style(value, styles) {
+ if (styles) {
+ var new_style = '';
+
+ /** @type {Record | undefined} */
+ var normal_styles;
+
+ /** @type {Record | undefined} */
+ var important_styles;
+
+ if (Array.isArray(styles)) {
+ normal_styles = styles[0];
+ important_styles = styles[1];
+ } else {
+ normal_styles = styles;
+ }
+
+ if (value) {
+ value = String(value)
+ .replaceAll(/\s*\/\*.*?\*\/\s*/g, '')
+ .trim();
+
+ /** @type {boolean | '"' | "'"} */
+ var in_str = false;
+ var in_apo = 0;
+ var in_comment = false;
+
+ var reserved_names = [];
+
+ if (normal_styles) {
+ reserved_names.push(...Object.keys(normal_styles).map(to_css_name));
+ }
+ if (important_styles) {
+ reserved_names.push(...Object.keys(important_styles).map(to_css_name));
+ }
+
+ var start_index = 0;
+ var name_index = -1;
+
+ const len = value.length;
+ for (var i = 0; i < len; i++) {
+ var c = value[i];
+
+ if (in_comment) {
+ if (c === '/' && value[i - 1] === '*') {
+ in_comment = false;
+ }
+ } else if (in_str) {
+ if (in_str === c) {
+ in_str = false;
+ }
+ } else if (c === '/' && value[i + 1] === '*') {
+ in_comment = true;
+ } else if (c === '"' || c === "'") {
+ in_str = c;
+ } else if (c === '(') {
+ in_apo++;
+ } else if (c === ')') {
+ in_apo--;
+ }
+
+ if (!in_comment && in_str === false && in_apo === 0) {
+ if (c === ':' && name_index === -1) {
+ name_index = i;
+ } else if (c === ';' || i === len - 1) {
+ if (name_index !== -1) {
+ var name = to_css_name(value.substring(start_index, name_index).trim());
+
+ if (!reserved_names.includes(name)) {
+ if (c !== ';') {
+ i++;
+ }
+
+ var property = value.substring(start_index, i).trim();
+ new_style += ' ' + property + ';';
+ }
+ }
+
+ start_index = i + 1;
+ name_index = -1;
+ }
+ }
+ }
+ }
+
+ if (normal_styles) {
+ new_style += append_styles(normal_styles);
+ }
+
+ if (important_styles) {
+ new_style += append_styles(important_styles, true);
+ }
+
+ new_style = new_style.trim();
+ return new_style === '' ? null : new_style;
+ }
+
+ return value == null ? null : String(value);
+}
diff --git a/packages/svelte/src/internal/shared/errors.js b/packages/svelte/src/internal/shared/errors.js
index 26d6822cdb29..6bcc35016a70 100644
--- a/packages/svelte/src/internal/shared/errors.js
+++ b/packages/svelte/src/internal/shared/errors.js
@@ -11,12 +11,29 @@ export function invalid_default_snippet() {
const error = new Error(`invalid_default_snippet\nCannot use \`{@render children(...)}\` if the parent component uses \`let:\` directives. Consider using a named snippet instead\nhttps://svelte.dev/e/invalid_default_snippet`);
error.name = 'Svelte error';
+
throw error;
} else {
throw new Error(`https://svelte.dev/e/invalid_default_snippet`);
}
}
+/**
+ * A snippet function was passed invalid arguments. Snippets should only be instantiated via `{@render ...}`
+ * @returns {never}
+ */
+export function invalid_snippet_arguments() {
+ if (DEV) {
+ const error = new Error(`invalid_snippet_arguments\nA snippet function was passed invalid arguments. Snippets should only be instantiated via \`{@render ...}\`\nhttps://svelte.dev/e/invalid_snippet_arguments`);
+
+ error.name = 'Svelte error';
+
+ throw error;
+ } else {
+ throw new Error(`https://svelte.dev/e/invalid_snippet_arguments`);
+ }
+}
+
/**
* `%name%(...)` can only be used during component initialisation
* @param {string} name
@@ -27,12 +44,29 @@ export function lifecycle_outside_component(name) {
const error = new Error(`lifecycle_outside_component\n\`${name}(...)\` can only be used during component initialisation\nhttps://svelte.dev/e/lifecycle_outside_component`);
error.name = 'Svelte error';
+
throw error;
} else {
throw new Error(`https://svelte.dev/e/lifecycle_outside_component`);
}
}
+/**
+ * Attempted to render a snippet without a `{@render}` block. This would cause the snippet code to be stringified instead of its content being rendered to the DOM. To fix this, change `{snippet}` to `{@render snippet()}`.
+ * @returns {never}
+ */
+export function snippet_without_render_tag() {
+ if (DEV) {
+ const error = new Error(`snippet_without_render_tag\nAttempted to render a snippet without a \`{@render}\` block. This would cause the snippet code to be stringified instead of its content being rendered to the DOM. To fix this, change \`{snippet}\` to \`{@render snippet()}\`.\nhttps://svelte.dev/e/snippet_without_render_tag`);
+
+ error.name = 'Svelte error';
+
+ throw error;
+ } else {
+ throw new Error(`https://svelte.dev/e/snippet_without_render_tag`);
+ }
+}
+
/**
* `%name%` is not a store with a `subscribe` method
* @param {string} name
@@ -43,6 +77,7 @@ export function store_invalid_shape(name) {
const error = new Error(`store_invalid_shape\n\`${name}\` is not a store with a \`subscribe\` method\nhttps://svelte.dev/e/store_invalid_shape`);
error.name = 'Svelte error';
+
throw error;
} else {
throw new Error(`https://svelte.dev/e/store_invalid_shape`);
@@ -58,6 +93,7 @@ export function svelte_element_invalid_this_value() {
const error = new Error(`svelte_element_invalid_this_value\nThe \`this\` prop on \`\` must be a string, if defined\nhttps://svelte.dev/e/svelte_element_invalid_this_value`);
error.name = 'Svelte error';
+
throw error;
} else {
throw new Error(`https://svelte.dev/e/svelte_element_invalid_this_value`);
diff --git a/packages/svelte/src/internal/shared/types.d.ts b/packages/svelte/src/internal/shared/types.d.ts
index a97a61af67a3..4deeb76b2f4b 100644
--- a/packages/svelte/src/internal/shared/types.d.ts
+++ b/packages/svelte/src/internal/shared/types.d.ts
@@ -3,10 +3,6 @@ export type Store = {
set(value: V): void;
};
-export type SourceLocation =
- | [line: number, column: number]
- | [line: number, column: number, SourceLocation[]];
-
export type Getters = {
[K in keyof T]: () => T[K];
};
diff --git a/packages/svelte/src/internal/shared/utils.js b/packages/svelte/src/internal/shared/utils.js
index f9d52cb065ac..10f8597520d9 100644
--- a/packages/svelte/src/internal/shared/utils.js
+++ b/packages/svelte/src/internal/shared/utils.js
@@ -10,6 +10,7 @@ export var get_descriptors = Object.getOwnPropertyDescriptors;
export var object_prototype = Object.prototype;
export var array_prototype = Array.prototype;
export var get_prototype_of = Object.getPrototypeOf;
+export var is_extensible = Object.isExtensible;
/**
* @param {any} thing
@@ -80,3 +81,38 @@ export function fallback(value, fallback, lazy = false) {
: /** @type {V} */ (fallback)
: value;
}
+
+/**
+ * When encountering a situation like `let [a, b, c] = $derived(blah())`,
+ * we need to stash an intermediate value that `a`, `b`, and `c` derive
+ * from, in case it's an iterable
+ * @template T
+ * @param {ArrayLike | Iterable} value
+ * @param {number} [n]
+ * @returns {Array}
+ */
+export function to_array(value, n) {
+ // return arrays unchanged
+ if (Array.isArray(value)) {
+ return value;
+ }
+
+ // if value is not iterable, or `n` is unspecified (indicates a rest
+ // element, which means we're not concerned about unbounded iterables)
+ // convert to an array with `Array.from`
+ if (n === undefined || !(Symbol.iterator in value)) {
+ return Array.from(value);
+ }
+
+ // otherwise, populate an array with `n` values
+
+ /** @type {T[]} */
+ const array = [];
+
+ for (const element of value) {
+ array.push(element);
+ if (array.length === n) break;
+ }
+
+ return array;
+}
diff --git a/packages/svelte/src/internal/shared/validate.js b/packages/svelte/src/internal/shared/validate.js
index 852c0e83bfb1..bbb237594bec 100644
--- a/packages/svelte/src/internal/shared/validate.js
+++ b/packages/svelte/src/internal/shared/validate.js
@@ -35,3 +35,15 @@ export function validate_store(store, name) {
e.store_invalid_shape(name);
}
}
+
+/**
+ * @template {() => unknown} T
+ * @param {T} fn
+ */
+export function prevent_snippet_stringification(fn) {
+ fn.toString = () => {
+ e.snippet_without_render_tag();
+ return '';
+ };
+ return fn;
+}
diff --git a/packages/svelte/src/internal/shared/warnings.js b/packages/svelte/src/internal/shared/warnings.js
index 281be0838211..0acca4418410 100644
--- a/packages/svelte/src/internal/shared/warnings.js
+++ b/packages/svelte/src/internal/shared/warnings.js
@@ -25,11 +25,15 @@ export function dynamic_void_element_content(tag) {
*/
export function state_snapshot_uncloneable(properties) {
if (DEV) {
- console.warn(`%c[svelte] state_snapshot_uncloneable\n%c${properties
- ? `The following properties cannot be cloned with \`$state.snapshot\` — the return value contains the originals:
+ console.warn(
+ `%c[svelte] state_snapshot_uncloneable\n%c${properties
+ ? `The following properties cannot be cloned with \`$state.snapshot\` — the return value contains the originals:
${properties}`
- : 'Value cannot be cloned with `$state.snapshot` — the original value was returned'}\nhttps://svelte.dev/e/state_snapshot_uncloneable`, bold, normal);
+ : 'Value cannot be cloned with `$state.snapshot` — the original value was returned'}\nhttps://svelte.dev/e/state_snapshot_uncloneable`,
+ bold,
+ normal
+ );
} else {
console.warn(`https://svelte.dev/e/state_snapshot_uncloneable`);
}
diff --git a/packages/svelte/src/legacy/legacy-client.js b/packages/svelte/src/legacy/legacy-client.js
index bb9a5a9c039b..45c478ecab1e 100644
--- a/packages/svelte/src/legacy/legacy-client.js
+++ b/packages/svelte/src/legacy/legacy-client.js
@@ -82,7 +82,7 @@ class Svelte4Component {
* @param {unknown} value
*/
var add_source = (key, value) => {
- var s = mutable_source(value);
+ var s = mutable_source(value, false, false);
sources.set(key, s);
return s;
};
diff --git a/packages/svelte/src/motion/spring.js b/packages/svelte/src/motion/spring.js
index 1c8e5f15c048..0f3bc6fb9f87 100644
--- a/packages/svelte/src/motion/spring.js
+++ b/packages/svelte/src/motion/spring.js
@@ -7,8 +7,10 @@ import { raf } from '../internal/client/timing.js';
import { is_date } from './utils.js';
import { set, source } from '../internal/client/reactivity/sources.js';
import { render_effect } from '../internal/client/reactivity/effects.js';
+import { tag } from '../internal/client/dev/tracing.js';
import { get } from '../internal/client/runtime.js';
import { deferred, noop } from '../internal/shared/utils.js';
+import { DEV } from 'esm-env';
/**
* @template T
@@ -172,8 +174,8 @@ export class Spring {
#damping = source(0.8);
#precision = source(0.01);
- #current = source(/** @type {T} */ (undefined));
- #target = source(/** @type {T} */ (undefined));
+ #current;
+ #target;
#last_value = /** @type {T} */ (undefined);
#last_time = 0;
@@ -192,11 +194,20 @@ export class Spring {
* @param {SpringOpts} [options]
*/
constructor(value, options = {}) {
- this.#current.v = this.#target.v = value;
+ this.#current = DEV ? tag(source(value), 'Spring.current') : source(value);
+ this.#target = DEV ? tag(source(value), 'Spring.target') : source(value);
if (typeof options.stiffness === 'number') this.#stiffness.v = clamp(options.stiffness, 0, 1);
if (typeof options.damping === 'number') this.#damping.v = clamp(options.damping, 0, 1);
if (typeof options.precision === 'number') this.#precision.v = options.precision;
+
+ if (DEV) {
+ tag(this.#stiffness, 'Spring.stiffness');
+ tag(this.#damping, 'Spring.damping');
+ tag(this.#precision, 'Spring.precision');
+ tag(this.#current, 'Spring.current');
+ tag(this.#target, 'Spring.target');
+ }
}
/**
diff --git a/packages/svelte/src/motion/tweened.js b/packages/svelte/src/motion/tweened.js
index 31cddf8f30f0..09bd06c325d5 100644
--- a/packages/svelte/src/motion/tweened.js
+++ b/packages/svelte/src/motion/tweened.js
@@ -7,7 +7,9 @@ import { loop } from '../internal/client/loop.js';
import { linear } from '../easing/index.js';
import { is_date } from './utils.js';
import { set, source } from '../internal/client/reactivity/sources.js';
+import { tag } from '../internal/client/dev/tracing.js';
import { get, render_effect } from 'svelte/internal/client';
+import { DEV } from 'esm-env';
/**
* @template T
@@ -175,8 +177,8 @@ export function tweened(value, defaults = {}) {
* @since 5.8.0
*/
export class Tween {
- #current = source(/** @type {T} */ (undefined));
- #target = source(/** @type {T} */ (undefined));
+ #current;
+ #target;
/** @type {TweenedOptions} */
#defaults;
@@ -189,8 +191,14 @@ export class Tween {
* @param {TweenedOptions} options
*/
constructor(value, options = {}) {
- this.#current.v = this.#target.v = value;
+ this.#current = source(value);
+ this.#target = source(value);
this.#defaults = options;
+
+ if (DEV) {
+ tag(this.#current, 'Tween.current');
+ tag(this.#target, 'Tween.target');
+ }
}
/**
diff --git a/packages/svelte/src/reactivity/create-subscriber.js b/packages/svelte/src/reactivity/create-subscriber.js
index 63deca62ea8b..491ffb45cba7 100644
--- a/packages/svelte/src/reactivity/create-subscriber.js
+++ b/packages/svelte/src/reactivity/create-subscriber.js
@@ -1,7 +1,9 @@
import { get, tick, untrack } from '../internal/client/runtime.js';
import { effect_tracking, render_effect } from '../internal/client/reactivity/effects.js';
import { source } from '../internal/client/reactivity/sources.js';
+import { tag } from '../internal/client/dev/tracing.js';
import { increment } from './utils.js';
+import { DEV } from 'esm-env';
/**
* Returns a `subscribe` function that, if called in an effect (including expressions in the template),
@@ -51,6 +53,10 @@ export function createSubscriber(start) {
/** @type {(() => void) | void} */
let stop;
+ if (DEV) {
+ tag(version, 'createSubscriber version');
+ }
+
return () => {
if (effect_tracking()) {
get(version);
diff --git a/packages/svelte/src/reactivity/date.js b/packages/svelte/src/reactivity/date.js
index 33da2e176159..8e2b5d41ab94 100644
--- a/packages/svelte/src/reactivity/date.js
+++ b/packages/svelte/src/reactivity/date.js
@@ -1,12 +1,46 @@
/** @import { Source } from '#client' */
import { derived } from '../internal/client/index.js';
-import { source, set } from '../internal/client/reactivity/sources.js';
+import { set, state } from '../internal/client/reactivity/sources.js';
+import { tag } from '../internal/client/dev/tracing.js';
import { active_reaction, get, set_active_reaction } from '../internal/client/runtime.js';
+import { DEV } from 'esm-env';
var inited = false;
+/**
+ * A reactive version of the built-in [`Date`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date) object.
+ * Reading the date (whether with methods like `date.getTime()` or `date.toString()`, or via things like [`Intl.DateTimeFormat`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat))
+ * in an [effect](https://svelte.dev/docs/svelte/$effect) or [derived](https://svelte.dev/docs/svelte/$derived)
+ * will cause it to be re-evaluated when the value of the date changes.
+ *
+ * ```svelte
+ *
+ *
+ * The time is {formatter.format(date)}
+ * ```
+ */
export class SvelteDate extends Date {
- #time = source(super.getTime());
+ #time = state(super.getTime());
/** @type {Map>} */
#deriveds = new Map();
@@ -17,6 +51,11 @@ export class SvelteDate extends Date {
constructor(...params) {
// @ts-ignore
super(...params);
+
+ if (DEV) {
+ tag(this.#time, 'SvelteDate.#time');
+ }
+
if (!inited) this.#init();
}
diff --git a/packages/svelte/src/reactivity/map.js b/packages/svelte/src/reactivity/map.js
index 3fa2945ef08c..cd2fac163fc6 100644
--- a/packages/svelte/src/reactivity/map.js
+++ b/packages/svelte/src/reactivity/map.js
@@ -1,10 +1,52 @@
/** @import { Source } from '#client' */
import { DEV } from 'esm-env';
-import { set, source } from '../internal/client/reactivity/sources.js';
+import { set, source, state } from '../internal/client/reactivity/sources.js';
+import { label, tag } from '../internal/client/dev/tracing.js';
import { get } from '../internal/client/runtime.js';
import { increment } from './utils.js';
/**
+ * A reactive version of the built-in [`Map`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map) object.
+ * Reading contents of the map (by iterating, or by reading `map.size` or calling `map.get(...)` or `map.has(...)` as in the [tic-tac-toe example](https://svelte.dev/playground/0b0ff4aa49c9443f9b47fe5203c78293) below) in an [effect](https://svelte.dev/docs/svelte/$effect) or [derived](https://svelte.dev/docs/svelte/$derived)
+ * will cause it to be re-evaluated as necessary when the map is updated.
+ *
+ * Note that values in a reactive map are _not_ made [deeply reactive](https://svelte.dev/docs/svelte/$state#Deep-state).
+ *
+ * ```svelte
+ *
+ *
+ *
+ * {#each Array(9), i}
+ * {
+ * board.set(i, player);
+ * player = player === 'x' ? 'o' : 'x';
+ * }}
+ * >{board.get(i)}
+ * {/each}
+ *
+ *
+ * {#if winner}
+ * {winner} wins!
+ * reset
+ * {:else}
+ * {player} is next
+ * {/if}
+ * ```
+ *
* @template K
* @template V
* @extends {Map}
@@ -12,8 +54,8 @@ import { increment } from './utils.js';
export class SvelteMap extends Map {
/** @type {Map>} */
#sources = new Map();
- #version = source(0);
- #size = source(0);
+ #version = state(0);
+ #size = state(0);
/**
* @param {Iterable | null | undefined} [value]
@@ -21,8 +63,13 @@ export class SvelteMap extends Map {
constructor(value) {
super();
- // If the value is invalid then the native exception will fire here
- if (DEV) value = new Map(value);
+ if (DEV) {
+ // If the value is invalid then the native exception will fire here
+ value = new Map(value);
+
+ tag(this.#version, 'SvelteMap version');
+ tag(this.#size, 'SvelteMap.size');
+ }
if (value) {
for (var [key, v] of value) {
@@ -41,6 +88,11 @@ export class SvelteMap extends Map {
var ret = super.get(key);
if (ret !== undefined) {
s = source(0);
+
+ if (DEV) {
+ tag(s, `SvelteMap get(${label(key)})`);
+ }
+
sources.set(key, s);
} else {
// We should always track the version in case
@@ -72,6 +124,11 @@ export class SvelteMap extends Map {
var ret = super.get(key);
if (ret !== undefined) {
s = source(0);
+
+ if (DEV) {
+ tag(s, `SvelteMap get(${label(key)})`);
+ }
+
sources.set(key, s);
} else {
// We should always track the version in case
@@ -97,7 +154,13 @@ export class SvelteMap extends Map {
var version = this.#version;
if (s === undefined) {
- sources.set(key, source(0));
+ s = source(0);
+
+ if (DEV) {
+ tag(s, `SvelteMap get(${label(key)})`);
+ }
+
+ sources.set(key, s);
set(this.#size, super.size);
increment(version);
} else if (prev_res !== value) {
@@ -156,12 +219,18 @@ export class SvelteMap extends Map {
if (this.#size.v !== sources.size) {
for (var key of super.keys()) {
if (!sources.has(key)) {
- sources.set(key, source(0));
+ var s = source(0);
+
+ if (DEV) {
+ tag(s, `SvelteMap get(${label(key)})`);
+ }
+
+ sources.set(key, s);
}
}
}
- for (var [, s] of this.#sources) {
+ for ([, s] of this.#sources) {
get(s);
}
}
diff --git a/packages/svelte/src/reactivity/media-query.js b/packages/svelte/src/reactivity/media-query.js
index 22310df18d53..d2867097195c 100644
--- a/packages/svelte/src/reactivity/media-query.js
+++ b/packages/svelte/src/reactivity/media-query.js
@@ -3,6 +3,19 @@ import { ReactiveValue } from './reactive-value.js';
const parenthesis_regex = /\(.+\)/;
+// these keywords are valid media queries but they need to be without parenthesis
+//
+// eg: new MediaQuery('screen')
+//
+// however because of the auto-parenthesis logic in the constructor since there's no parentehesis
+// in the media query they'll be surrounded by parenthesis
+//
+// however we can check if the media query is only composed of these keywords
+// and skip the auto-parenthesis
+//
+// https://github.com/sveltejs/svelte/issues/15930
+const non_parenthesized_keywords = new Set(['all', 'print', 'screen', 'and', 'or', 'not', 'only']);
+
/**
* Creates a media query and provides a `current` property that reflects whether or not it matches.
*
@@ -27,7 +40,12 @@ export class MediaQuery extends ReactiveValue {
* @param {boolean} [fallback] Fallback value for the server
*/
constructor(query, fallback) {
- let final_query = parenthesis_regex.test(query) ? query : `(${query})`;
+ let final_query =
+ parenthesis_regex.test(query) ||
+ // we need to use `some` here because technically this `window.matchMedia('random,screen')` still returns true
+ query.split(/[\s,]+/).some((keyword) => non_parenthesized_keywords.has(keyword.trim()))
+ ? query
+ : `(${query})`;
const q = window.matchMedia(final_query);
super(
() => q.matches,
diff --git a/packages/svelte/src/reactivity/set.js b/packages/svelte/src/reactivity/set.js
index be0c2d2cf5d6..8a656c2bc14a 100644
--- a/packages/svelte/src/reactivity/set.js
+++ b/packages/svelte/src/reactivity/set.js
@@ -1,6 +1,7 @@
/** @import { Source } from '#client' */
import { DEV } from 'esm-env';
-import { source, set } from '../internal/client/reactivity/sources.js';
+import { source, set, state } from '../internal/client/reactivity/sources.js';
+import { label, tag } from '../internal/client/dev/tracing.js';
import { get } from '../internal/client/runtime.js';
import { increment } from './utils.js';
@@ -10,14 +11,45 @@ var set_like_methods = ['difference', 'intersection', 'symmetricDifference', 'un
var inited = false;
/**
+ * A reactive version of the built-in [`Set`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set) object.
+ * Reading contents of the set (by iterating, or by reading `set.size` or calling `set.has(...)` as in the [example](https://svelte.dev/playground/53438b51194b4882bcc18cddf9f96f15) below) in an [effect](https://svelte.dev/docs/svelte/$effect) or [derived](https://svelte.dev/docs/svelte/$derived)
+ * will cause it to be re-evaluated as necessary when the set is updated.
+ *
+ * Note that values in a reactive set are _not_ made [deeply reactive](https://svelte.dev/docs/svelte/$state#Deep-state).
+ *
+ * ```svelte
+ *
+ *
+ * {#each ['🙈', '🙉', '🙊'] as monkey}
+ * toggle(monkey)}>{monkey}
+ * {/each}
+ *
+ * monkeys.clear()}>clear
+ *
+ * {#if monkeys.has('🙈')}see no evil
{/if}
+ * {#if monkeys.has('🙉')}hear no evil
{/if}
+ * {#if monkeys.has('🙊')}speak no evil
{/if}
+ * ```
+ *
* @template T
* @extends {Set}
*/
export class SvelteSet extends Set {
/** @type {Map>} */
#sources = new Map();
- #version = source(0);
- #size = source(0);
+ #version = state(0);
+ #size = state(0);
/**
* @param {Iterable | null | undefined} [value]
@@ -25,8 +57,13 @@ export class SvelteSet extends Set {
constructor(value) {
super();
- // If the value is invalid then the native exception will fire here
- if (DEV) value = new Set(value);
+ if (DEV) {
+ // If the value is invalid then the native exception will fire here
+ value = new Set(value);
+
+ tag(this.#version, 'SvelteSet version');
+ tag(this.#size, 'SvelteSet.size');
+ }
if (value) {
for (var element of value) {
@@ -80,6 +117,11 @@ export class SvelteSet extends Set {
}
s = source(true);
+
+ if (DEV) {
+ tag(s, `SvelteSet has(${label(value)})`);
+ }
+
sources.set(value, s);
}
diff --git a/packages/svelte/src/reactivity/url-search-params.js b/packages/svelte/src/reactivity/url-search-params.js
index 13f697199643..389da7cdb67a 100644
--- a/packages/svelte/src/reactivity/url-search-params.js
+++ b/packages/svelte/src/reactivity/url-search-params.js
@@ -1,12 +1,40 @@
-import { source } from '../internal/client/reactivity/sources.js';
+import { DEV } from 'esm-env';
+import { state } from '../internal/client/reactivity/sources.js';
+import { tag } from '../internal/client/dev/tracing.js';
import { get } from '../internal/client/runtime.js';
import { get_current_url } from './url.js';
import { increment } from './utils.js';
export const REPLACE = Symbol();
+/**
+ * A reactive version of the built-in [`URLSearchParams`](https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams) object.
+ * Reading its contents (by iterating, or by calling `params.get(...)` or `params.getAll(...)` as in the [example](https://svelte.dev/playground/b3926c86c5384bab9f2cf993bc08c1c8) below) in an [effect](https://svelte.dev/docs/svelte/$effect) or [derived](https://svelte.dev/docs/svelte/$derived)
+ * will cause it to be re-evaluated as necessary when the params are updated.
+ *
+ * ```svelte
+ *
+ *
+ *
+ *
+ * params.append(key, value)}>append
+ *
+ * ?{params.toString()}
+ *
+ * {#each params as [key, value]}
+ * {key}: {value}
+ * {/each}
+ * ```
+ */
export class SvelteURLSearchParams extends URLSearchParams {
- #version = source(0);
+ #version = DEV ? tag(state(0), 'SvelteURLSearchParams version') : state(0);
#url = get_current_url();
#updating = false;
@@ -23,6 +51,7 @@ export class SvelteURLSearchParams extends URLSearchParams {
/**
* @param {URLSearchParams} params
+ * @internal
*/
[REPLACE](params) {
if (this.#updating) return;
diff --git a/packages/svelte/src/reactivity/url.js b/packages/svelte/src/reactivity/url.js
index 5d003be0210a..dfede23f6ee2 100644
--- a/packages/svelte/src/reactivity/url.js
+++ b/packages/svelte/src/reactivity/url.js
@@ -1,4 +1,6 @@
-import { source, set } from '../internal/client/reactivity/sources.js';
+import { DEV } from 'esm-env';
+import { set, state } from '../internal/client/reactivity/sources.js';
+import { tag } from '../internal/client/dev/tracing.js';
import { get } from '../internal/client/runtime.js';
import { REPLACE, SvelteURLSearchParams } from './url-search-params.js';
@@ -10,15 +12,42 @@ export function get_current_url() {
return current_url;
}
+/**
+ * A reactive version of the built-in [`URL`](https://developer.mozilla.org/en-US/docs/Web/API/URL) object.
+ * Reading properties of the URL (such as `url.href` or `url.pathname`) in an [effect](https://svelte.dev/docs/svelte/$effect) or [derived](https://svelte.dev/docs/svelte/$derived)
+ * will cause it to be re-evaluated as necessary when the URL changes.
+ *
+ * The `searchParams` property is an instance of [SvelteURLSearchParams](https://svelte.dev/docs/svelte/svelte-reactivity#SvelteURLSearchParams).
+ *
+ * [Example](https://svelte.dev/playground/5a694758901b448c83dc40dc31c71f2a):
+ *
+ * ```svelte
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ * ```
+ */
export class SvelteURL extends URL {
- #protocol = source(super.protocol);
- #username = source(super.username);
- #password = source(super.password);
- #hostname = source(super.hostname);
- #port = source(super.port);
- #pathname = source(super.pathname);
- #hash = source(super.hash);
- #search = source(super.search);
+ #protocol = state(super.protocol);
+ #username = state(super.username);
+ #password = state(super.password);
+ #hostname = state(super.hostname);
+ #port = state(super.port);
+ #pathname = state(super.pathname);
+ #hash = state(super.hash);
+ #search = state(super.search);
#searchParams;
/**
@@ -29,6 +58,17 @@ export class SvelteURL extends URL {
url = new URL(url, base);
super(url);
+ if (DEV) {
+ tag(this.#protocol, 'SvelteURL.protocol');
+ tag(this.#username, 'SvelteURL.username');
+ tag(this.#password, 'SvelteURL.password');
+ tag(this.#hostname, 'SvelteURL.hostname');
+ tag(this.#port, 'SvelteURL.port');
+ tag(this.#pathname, 'SvelteURL.pathname');
+ tag(this.#hash, 'SvelteURL.hash');
+ tag(this.#search, 'SvelteURL.search');
+ }
+
current_url = this;
this.#searchParams = new SvelteURLSearchParams(url.searchParams);
current_url = null;
diff --git a/packages/svelte/src/reactivity/window/index.js b/packages/svelte/src/reactivity/window/index.js
index 8c50a5c440df..d4fcf29e4e12 100644
--- a/packages/svelte/src/reactivity/window/index.js
+++ b/packages/svelte/src/reactivity/window/index.js
@@ -1,8 +1,9 @@
-import { BROWSER } from 'esm-env';
+import { BROWSER, DEV } from 'esm-env';
import { on } from '../../events/index.js';
import { ReactiveValue } from '../reactive-value.js';
import { get } from '../../internal/client/index.js';
import { set, source } from '../../internal/client/reactivity/sources.js';
+import { tag } from '../../internal/client/dev/tracing.js';
/**
* `scrollX.current` is a reactive view of `window.scrollX`. On the server it is `undefined`.
@@ -147,6 +148,10 @@ export const devicePixelRatio = /* @__PURE__ */ new (class DevicePixelRatio {
if (BROWSER) {
this.#update();
}
+
+ if (DEV) {
+ tag(this.#dpr, 'window.devicePixelRatio');
+ }
}
get current() {
diff --git a/packages/svelte/src/store/index-client.js b/packages/svelte/src/store/index-client.js
index ae6806ec763f..2f0a1a831a0c 100644
--- a/packages/svelte/src/store/index-client.js
+++ b/packages/svelte/src/store/index-client.js
@@ -6,6 +6,12 @@ import {
} from '../internal/client/reactivity/effects.js';
import { get, writable } from './shared/index.js';
import { createSubscriber } from '../reactivity/create-subscriber.js';
+import {
+ active_effect,
+ active_reaction,
+ set_active_effect,
+ set_active_reaction
+} from '../internal/client/runtime.js';
export { derived, get, readable, readonly, writable } from './shared/index.js';
@@ -39,19 +45,34 @@ export { derived, get, readable, readonly, writable } from './shared/index.js';
* @returns {Writable | Readable}
*/
export function toStore(get, set) {
- let init_value = get();
+ var effect = active_effect;
+ var reaction = active_reaction;
+ var init_value = get();
+
const store = writable(init_value, (set) => {
// If the value has changed before we call subscribe, then
// we need to treat the value as already having run
- let ran = init_value !== get();
+ var ran = init_value !== get();
// TODO do we need a different implementation on the server?
- const teardown = effect_root(() => {
- render_effect(() => {
- const value = get();
- if (ran) set(value);
+ var teardown;
+ // Apply the reaction and effect at the time of toStore being called
+ var previous_reaction = active_reaction;
+ var previous_effect = active_effect;
+ set_active_reaction(reaction);
+ set_active_effect(effect);
+
+ try {
+ teardown = effect_root(() => {
+ render_effect(() => {
+ const value = get();
+ if (ran) set(value);
+ });
});
- });
+ } finally {
+ set_active_reaction(previous_reaction);
+ set_active_effect(previous_effect);
+ }
ran = true;
diff --git a/packages/svelte/src/utils.js b/packages/svelte/src/utils.js
index d4d106d56deb..921eaec57cf5 100644
--- a/packages/svelte/src/utils.js
+++ b/packages/svelte/src/utils.js
@@ -428,15 +428,19 @@ export function is_mathml(name) {
return MATHML_ELEMENTS.includes(name);
}
-const RUNES = /** @type {const} */ ([
+export const STATE_CREATION_RUNES = /** @type {const} */ ([
'$state',
'$state.raw',
+ '$derived',
+ '$derived.by'
+]);
+
+const RUNES = /** @type {const} */ ([
+ ...STATE_CREATION_RUNES,
'$state.snapshot',
'$props',
'$props.id',
'$bindable',
- '$derived',
- '$derived.by',
'$effect',
'$effect.pre',
'$effect.tracking',
@@ -447,12 +451,24 @@ const RUNES = /** @type {const} */ ([
'$host'
]);
+/** @typedef {RUNES[number]} RuneName */
+
/**
* @param {string} name
- * @returns {name is RUNES[number]}
+ * @returns {name is RuneName}
*/
export function is_rune(name) {
- return RUNES.includes(/** @type {RUNES[number]} */ (name));
+ return RUNES.includes(/** @type {RuneName} */ (name));
+}
+
+/** @typedef {STATE_CREATION_RUNES[number]} StateCreationRuneName */
+
+/**
+ * @param {string} name
+ * @returns {name is StateCreationRuneName}
+ */
+export function is_state_creation_rune(name) {
+ return STATE_CREATION_RUNES.includes(/** @type {StateCreationRuneName} */ (name));
}
/** List of elements that require raw contents and should not have SSR comments put in them */
@@ -465,8 +481,10 @@ export function is_raw_text_element(name) {
/**
* Prevent devtools trying to make `location` a clickable link by inserting a zero-width space
- * @param {string | undefined} location
+ * @template {string | undefined} T
+ * @param {T} location
+ * @returns {T};
*/
export function sanitize_location(location) {
- return location?.replace(/\//g, '/\u200b');
+ return /** @type {T} */ (location?.replace(/\//g, '/\u200b'));
}
diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js
index d00562ea4ee0..47c354c7a21e 100644
--- a/packages/svelte/src/version.js
+++ b/packages/svelte/src/version.js
@@ -4,5 +4,5 @@
* The current version, as set in package.json.
* @type {string}
*/
-export const VERSION = '5.22.2';
+export const VERSION = '5.35.2';
export const PUBLIC_VERSION = '5';
diff --git a/packages/svelte/tests/compiler-errors/samples/class-state-field-static/_config.js b/packages/svelte/tests/compiler-errors/samples/class-state-field-static/_config.js
index e6551031cc8e..f5b116117ed3 100644
--- a/packages/svelte/tests/compiler-errors/samples/class-state-field-static/_config.js
+++ b/packages/svelte/tests/compiler-errors/samples/class-state-field-static/_config.js
@@ -4,7 +4,7 @@ export default test({
error: {
code: 'state_invalid_placement',
message:
- '`$state(...)` can only be used as a variable declaration initializer or a class field',
+ '`$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.',
position: [33, 41]
}
});
diff --git a/packages/svelte/tests/compiler-errors/samples/css-global-block-in-pseudoclass/_config.js b/packages/svelte/tests/compiler-errors/samples/css-global-block-in-pseudoclass/_config.js
new file mode 100644
index 000000000000..c987725d743c
--- /dev/null
+++ b/packages/svelte/tests/compiler-errors/samples/css-global-block-in-pseudoclass/_config.js
@@ -0,0 +1,9 @@
+import { test } from '../../test';
+
+export default test({
+ error: {
+ code: 'css_global_block_invalid_placement',
+ message: 'A `:global` selector cannot be inside a pseudoclass',
+ position: [28, 35]
+ }
+});
diff --git a/packages/svelte/tests/compiler-errors/samples/css-global-block-in-pseudoclass/main.svelte b/packages/svelte/tests/compiler-errors/samples/css-global-block-in-pseudoclass/main.svelte
new file mode 100644
index 000000000000..53060df97da0
--- /dev/null
+++ b/packages/svelte/tests/compiler-errors/samples/css-global-block-in-pseudoclass/main.svelte
@@ -0,0 +1,4 @@
+
diff --git a/packages/svelte/tests/compiler-errors/samples/css-global-block-multiple-1/_config.js b/packages/svelte/tests/compiler-errors/samples/css-global-block-multiple-1/_config.js
new file mode 100644
index 000000000000..85dedc801221
--- /dev/null
+++ b/packages/svelte/tests/compiler-errors/samples/css-global-block-multiple-1/_config.js
@@ -0,0 +1,10 @@
+import { test } from '../../test';
+
+export default test({
+ error: {
+ code: 'css_global_block_invalid_list',
+ message:
+ "A `:global` selector cannot be part of a selector list with entries that don't contain `:global`",
+ position: [232, 246]
+ }
+});
diff --git a/packages/svelte/tests/compiler-errors/samples/css-global-block-multiple-1/main.svelte b/packages/svelte/tests/compiler-errors/samples/css-global-block-multiple-1/main.svelte
new file mode 100644
index 000000000000..260921f7048d
--- /dev/null
+++ b/packages/svelte/tests/compiler-errors/samples/css-global-block-multiple-1/main.svelte
@@ -0,0 +1,9 @@
+
diff --git a/packages/svelte/tests/compiler-errors/samples/css-global-block-multiple-2/_config.js b/packages/svelte/tests/compiler-errors/samples/css-global-block-multiple-2/_config.js
new file mode 100644
index 000000000000..f24095800a6d
--- /dev/null
+++ b/packages/svelte/tests/compiler-errors/samples/css-global-block-multiple-2/_config.js
@@ -0,0 +1,10 @@
+import { test } from '../../test';
+
+export default test({
+ error: {
+ code: 'css_global_block_invalid_list',
+ message:
+ "A `:global` selector cannot be part of a selector list with entries that don't contain `:global`",
+ position: [24, 43]
+ }
+});
diff --git a/packages/svelte/tests/compiler-errors/samples/css-global-block-multiple-2/main.svelte b/packages/svelte/tests/compiler-errors/samples/css-global-block-multiple-2/main.svelte
new file mode 100644
index 000000000000..2a09ec10cea5
--- /dev/null
+++ b/packages/svelte/tests/compiler-errors/samples/css-global-block-multiple-2/main.svelte
@@ -0,0 +1,6 @@
+
diff --git a/packages/svelte/tests/compiler-errors/samples/css-global-block-multiple/_config.js b/packages/svelte/tests/compiler-errors/samples/css-global-block-multiple/_config.js
deleted file mode 100644
index 9ae4e758c46f..000000000000
--- a/packages/svelte/tests/compiler-errors/samples/css-global-block-multiple/_config.js
+++ /dev/null
@@ -1,9 +0,0 @@
-import { test } from '../../test';
-
-export default test({
- error: {
- code: 'css_global_block_invalid_list',
- message: 'A `:global` selector cannot be part of a selector list with more than one item',
- position: [9, 31]
- }
-});
diff --git a/packages/svelte/tests/compiler-errors/samples/css-global-block-multiple/main.svelte b/packages/svelte/tests/compiler-errors/samples/css-global-block-multiple/main.svelte
deleted file mode 100644
index 75178bc664eb..000000000000
--- a/packages/svelte/tests/compiler-errors/samples/css-global-block-multiple/main.svelte
+++ /dev/null
@@ -1,3 +0,0 @@
-
diff --git a/packages/svelte/tests/compiler-errors/samples/runes-no-derived-state-field-assignment/_config.js b/packages/svelte/tests/compiler-errors/samples/invalid-rune-name-shadowed/_config.js
similarity index 50%
rename from packages/svelte/tests/compiler-errors/samples/runes-no-derived-state-field-assignment/_config.js
rename to packages/svelte/tests/compiler-errors/samples/invalid-rune-name-shadowed/_config.js
index 94985a99397a..a24996677cd3 100644
--- a/packages/svelte/tests/compiler-errors/samples/runes-no-derived-state-field-assignment/_config.js
+++ b/packages/svelte/tests/compiler-errors/samples/invalid-rune-name-shadowed/_config.js
@@ -2,7 +2,7 @@ import { test } from '../../test';
export default test({
error: {
- code: 'constant_assignment',
- message: 'Cannot assign to derived state'
+ code: 'rune_invalid_name',
+ message: '`$state.foo` is not a valid rune'
}
});
diff --git a/packages/svelte/tests/compiler-errors/samples/invalid-rune-name-shadowed/main.svelte.js b/packages/svelte/tests/compiler-errors/samples/invalid-rune-name-shadowed/main.svelte.js
new file mode 100644
index 000000000000..966f7bc63b61
--- /dev/null
+++ b/packages/svelte/tests/compiler-errors/samples/invalid-rune-name-shadowed/main.svelte.js
@@ -0,0 +1,5 @@
+class State {
+ value = $state.foo();
+}
+
+export const state = new State();
diff --git a/packages/svelte/tests/compiler-errors/samples/runes-no-derived-assignment/_config.js b/packages/svelte/tests/compiler-errors/samples/runes-no-derived-assignment/_config.js
deleted file mode 100644
index 94985a99397a..000000000000
--- a/packages/svelte/tests/compiler-errors/samples/runes-no-derived-assignment/_config.js
+++ /dev/null
@@ -1,8 +0,0 @@
-import { test } from '../../test';
-
-export default test({
- error: {
- code: 'constant_assignment',
- message: 'Cannot assign to derived state'
- }
-});
diff --git a/packages/svelte/tests/compiler-errors/samples/runes-no-derived-assignment/main.svelte b/packages/svelte/tests/compiler-errors/samples/runes-no-derived-assignment/main.svelte
deleted file mode 100644
index 3bf836f6c586..000000000000
--- a/packages/svelte/tests/compiler-errors/samples/runes-no-derived-assignment/main.svelte
+++ /dev/null
@@ -1,5 +0,0 @@
-
diff --git a/packages/svelte/tests/compiler-errors/samples/runes-no-derived-binding/_config.js b/packages/svelte/tests/compiler-errors/samples/runes-no-derived-binding/_config.js
deleted file mode 100644
index 87b88d79cc26..000000000000
--- a/packages/svelte/tests/compiler-errors/samples/runes-no-derived-binding/_config.js
+++ /dev/null
@@ -1,8 +0,0 @@
-import { test } from '../../test';
-
-export default test({
- error: {
- code: 'constant_binding',
- message: 'Cannot bind to derived state'
- }
-});
diff --git a/packages/svelte/tests/compiler-errors/samples/runes-no-derived-binding/main.svelte b/packages/svelte/tests/compiler-errors/samples/runes-no-derived-binding/main.svelte
deleted file mode 100644
index 6c198dc068fe..000000000000
--- a/packages/svelte/tests/compiler-errors/samples/runes-no-derived-binding/main.svelte
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
diff --git a/packages/svelte/tests/compiler-errors/samples/runes-no-derived-state-field-assignment/main.svelte b/packages/svelte/tests/compiler-errors/samples/runes-no-derived-state-field-assignment/main.svelte
deleted file mode 100644
index d44806757e6c..000000000000
--- a/packages/svelte/tests/compiler-errors/samples/runes-no-derived-state-field-assignment/main.svelte
+++ /dev/null
@@ -1,10 +0,0 @@
-
diff --git a/packages/svelte/tests/compiler-errors/samples/runes-no-derived-state-field-update/_config.js b/packages/svelte/tests/compiler-errors/samples/runes-no-derived-state-field-update/_config.js
deleted file mode 100644
index 94985a99397a..000000000000
--- a/packages/svelte/tests/compiler-errors/samples/runes-no-derived-state-field-update/_config.js
+++ /dev/null
@@ -1,8 +0,0 @@
-import { test } from '../../test';
-
-export default test({
- error: {
- code: 'constant_assignment',
- message: 'Cannot assign to derived state'
- }
-});
diff --git a/packages/svelte/tests/compiler-errors/samples/runes-no-derived-state-field-update/main.svelte b/packages/svelte/tests/compiler-errors/samples/runes-no-derived-state-field-update/main.svelte
deleted file mode 100644
index e4ee2e86356d..000000000000
--- a/packages/svelte/tests/compiler-errors/samples/runes-no-derived-state-field-update/main.svelte
+++ /dev/null
@@ -1,10 +0,0 @@
-
diff --git a/packages/svelte/tests/compiler-errors/samples/runes-no-derived-update/_config.js b/packages/svelte/tests/compiler-errors/samples/runes-no-derived-update/_config.js
deleted file mode 100644
index 94985a99397a..000000000000
--- a/packages/svelte/tests/compiler-errors/samples/runes-no-derived-update/_config.js
+++ /dev/null
@@ -1,8 +0,0 @@
-import { test } from '../../test';
-
-export default test({
- error: {
- code: 'constant_assignment',
- message: 'Cannot assign to derived state'
- }
-});
diff --git a/packages/svelte/tests/compiler-errors/samples/runes-no-derived-update/main.svelte b/packages/svelte/tests/compiler-errors/samples/runes-no-derived-update/main.svelte
deleted file mode 100644
index d266c95bb872..000000000000
--- a/packages/svelte/tests/compiler-errors/samples/runes-no-derived-update/main.svelte
+++ /dev/null
@@ -1,5 +0,0 @@
-
diff --git a/packages/svelte/tests/compiler-errors/samples/runes-no-rune-each/_config.js b/packages/svelte/tests/compiler-errors/samples/runes-no-rune-each/_config.js
index 1185ca6b4548..592b2634a389 100644
--- a/packages/svelte/tests/compiler-errors/samples/runes-no-rune-each/_config.js
+++ b/packages/svelte/tests/compiler-errors/samples/runes-no-rune-each/_config.js
@@ -3,6 +3,7 @@ import { test } from '../../test';
export default test({
error: {
code: 'state_invalid_placement',
- message: '`$state(...)` can only be used as a variable declaration initializer or a class field'
+ 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.'
}
});
diff --git a/packages/svelte/tests/compiler-errors/samples/runes-wrong-derived-placement/_config.js b/packages/svelte/tests/compiler-errors/samples/runes-wrong-derived-placement/_config.js
index 476d7239a307..f31748d26a40 100644
--- a/packages/svelte/tests/compiler-errors/samples/runes-wrong-derived-placement/_config.js
+++ b/packages/svelte/tests/compiler-errors/samples/runes-wrong-derived-placement/_config.js
@@ -4,6 +4,6 @@ export default test({
error: {
code: 'state_invalid_placement',
message:
- '`$derived(...)` can only be used as a variable declaration initializer or a class field'
+ '`$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.'
}
});
diff --git a/packages/svelte/tests/compiler-errors/samples/runes-wrong-state-placement/_config.js b/packages/svelte/tests/compiler-errors/samples/runes-wrong-state-placement/_config.js
index 1185ca6b4548..592b2634a389 100644
--- a/packages/svelte/tests/compiler-errors/samples/runes-wrong-state-placement/_config.js
+++ b/packages/svelte/tests/compiler-errors/samples/runes-wrong-state-placement/_config.js
@@ -3,6 +3,7 @@ import { test } from '../../test';
export default test({
error: {
code: 'state_invalid_placement',
- message: '`$state(...)` can only be used as a variable declaration initializer or a class field'
+ 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.'
}
});
diff --git a/packages/svelte/tests/compiler-errors/samples/runes-wrong-state-raw-args/_config.js b/packages/svelte/tests/compiler-errors/samples/runes-wrong-state-raw-args/_config.js
new file mode 100644
index 000000000000..af226559d11b
--- /dev/null
+++ b/packages/svelte/tests/compiler-errors/samples/runes-wrong-state-raw-args/_config.js
@@ -0,0 +1,8 @@
+import { test } from '../../test';
+
+export default test({
+ error: {
+ code: 'rune_invalid_arguments_length',
+ message: '`$state.raw` must be called with zero or one arguments'
+ }
+});
diff --git a/packages/svelte/tests/compiler-errors/samples/runes-wrong-state-raw-args/main.svelte b/packages/svelte/tests/compiler-errors/samples/runes-wrong-state-raw-args/main.svelte
new file mode 100644
index 000000000000..2b50b43b9a2b
--- /dev/null
+++ b/packages/svelte/tests/compiler-errors/samples/runes-wrong-state-raw-args/main.svelte
@@ -0,0 +1,3 @@
+
diff --git a/packages/svelte/tests/compiler-errors/samples/runes-wrong-state-raw-args/main.svelte.js b/packages/svelte/tests/compiler-errors/samples/runes-wrong-state-raw-args/main.svelte.js
new file mode 100644
index 000000000000..442aaad14289
--- /dev/null
+++ b/packages/svelte/tests/compiler-errors/samples/runes-wrong-state-raw-args/main.svelte.js
@@ -0,0 +1 @@
+const foo = $state.raw(1, 2, 3);
diff --git a/packages/svelte/tests/css/samples/attribute-selector-matches-derictive/expected.css b/packages/svelte/tests/css/samples/attribute-selector-matches-derictive/expected.css
new file mode 100644
index 000000000000..4b5e4bfd091f
--- /dev/null
+++ b/packages/svelte/tests/css/samples/attribute-selector-matches-derictive/expected.css
@@ -0,0 +1,2 @@
+ span[class].svelte-xyz { color: green }
+ div[style].svelte-xyz { color: green }
\ No newline at end of file
diff --git a/packages/svelte/tests/css/samples/attribute-selector-matches-derictive/input.svelte b/packages/svelte/tests/css/samples/attribute-selector-matches-derictive/input.svelte
new file mode 100644
index 000000000000..2f9ab202ca80
--- /dev/null
+++ b/packages/svelte/tests/css/samples/attribute-selector-matches-derictive/input.svelte
@@ -0,0 +1,7 @@
+
+
+
+
diff --git a/packages/svelte/tests/css/samples/class-directive/_config.js b/packages/svelte/tests/css/samples/class-directive/_config.js
new file mode 100644
index 000000000000..abeb8b632994
--- /dev/null
+++ b/packages/svelte/tests/css/samples/class-directive/_config.js
@@ -0,0 +1,20 @@
+import { test } from '../../test';
+
+export default test({
+ warnings: [
+ {
+ code: 'css_unused_selector',
+ message: 'Unused CSS selector ".forth"\nhttps://svelte.dev/e/css_unused_selector',
+ start: {
+ line: 8,
+ column: 2,
+ character: 190
+ },
+ end: {
+ line: 8,
+ column: 8,
+ character: 196
+ }
+ }
+ ]
+});
diff --git a/packages/svelte/tests/css/samples/class-directive/expected.css b/packages/svelte/tests/css/samples/class-directive/expected.css
new file mode 100644
index 000000000000..b3a74baee5c0
--- /dev/null
+++ b/packages/svelte/tests/css/samples/class-directive/expected.css
@@ -0,0 +1,5 @@
+
+ .zero.first.svelte-xyz { color: green }
+ .second.svelte-xyz { color: green }
+ .third.svelte-xyz { color: green }
+ /* (unused) .forth { color: red }*/
diff --git a/packages/svelte/tests/css/samples/class-directive/input.svelte b/packages/svelte/tests/css/samples/class-directive/input.svelte
new file mode 100644
index 000000000000..70075f89d49d
--- /dev/null
+++ b/packages/svelte/tests/css/samples/class-directive/input.svelte
@@ -0,0 +1,9 @@
+
+
+
+
\ No newline at end of file
diff --git a/packages/svelte/tests/css/samples/general-siblings-combinator-slot/_config.js b/packages/svelte/tests/css/samples/general-siblings-combinator-slot/_config.js
index 97e470d1c325..76448654957d 100644
--- a/packages/svelte/tests/css/samples/general-siblings-combinator-slot/_config.js
+++ b/packages/svelte/tests/css/samples/general-siblings-combinator-slot/_config.js
@@ -5,32 +5,20 @@ export default test({
{
code: 'css_unused_selector',
message: 'Unused CSS selector ".b ~ .c"',
- start: { character: 137, column: 1, line: 11 },
- end: { character: 144, column: 8, line: 11 }
+ start: { character: 191, column: 1, line: 13 },
+ end: { character: 198, column: 8, line: 13 }
},
{
code: 'css_unused_selector',
message: 'Unused CSS selector ".c ~ .f"',
- start: { character: 162, column: 1, line: 12 },
- end: { character: 169, column: 8, line: 12 }
- },
- {
- code: 'css_unused_selector',
- message: 'Unused CSS selector ".f ~ .g"',
- start: { character: 187, column: 1, line: 13 },
- end: { character: 194, column: 8, line: 13 }
+ start: { character: 216, column: 1, line: 14 },
+ end: { character: 223, column: 8, line: 14 }
},
{
code: 'css_unused_selector',
message: 'Unused CSS selector ".b ~ .f"',
- start: { character: 212, column: 1, line: 14 },
- end: { character: 219, column: 8, line: 14 }
- },
- {
- code: 'css_unused_selector',
- message: 'Unused CSS selector ".b ~ .g"',
- start: { character: 237, column: 1, line: 15 },
- end: { character: 244, column: 8, line: 15 }
+ start: { character: 241, column: 1, line: 15 },
+ end: { character: 248, column: 8, line: 15 }
}
]
});
diff --git a/packages/svelte/tests/css/samples/general-siblings-combinator-slot/expected.css b/packages/svelte/tests/css/samples/general-siblings-combinator-slot/expected.css
index 67a19d10c996..53fca3ae9e04 100644
--- a/packages/svelte/tests/css/samples/general-siblings-combinator-slot/expected.css
+++ b/packages/svelte/tests/css/samples/general-siblings-combinator-slot/expected.css
@@ -2,10 +2,10 @@
.d.svelte-xyz ~ .e:where(.svelte-xyz) { color: green; }
.a.svelte-xyz ~ .g:where(.svelte-xyz) { color: green; }
.a.svelte-xyz ~ .b:where(.svelte-xyz) { color: green; }
+ .f.svelte-xyz ~ .g:where(.svelte-xyz) { color: green; }
+ .b.svelte-xyz ~ .g:where(.svelte-xyz) { color: green; }
/* no match */
/* (unused) .b ~ .c { color: red; }*/
/* (unused) .c ~ .f { color: red; }*/
- /* (unused) .f ~ .g { color: red; }*/
/* (unused) .b ~ .f { color: red; }*/
- /* (unused) .b ~ .g { color: red; }*/
diff --git a/packages/svelte/tests/css/samples/general-siblings-combinator-slot/input.svelte b/packages/svelte/tests/css/samples/general-siblings-combinator-slot/input.svelte
index 2e2846fa87a6..52264d3a5a25 100644
--- a/packages/svelte/tests/css/samples/general-siblings-combinator-slot/input.svelte
+++ b/packages/svelte/tests/css/samples/general-siblings-combinator-slot/input.svelte
@@ -6,13 +6,13 @@
.d ~ .e { color: green; }
.a ~ .g { color: green; }
.a ~ .b { color: green; }
+ .f ~ .g { color: green; }
+ .b ~ .g { color: green; }
/* no match */
.b ~ .c { color: red; }
.c ~ .f { color: red; }
- .f ~ .g { color: red; }
.b ~ .f { color: red; }
- .b ~ .g { color: red; }
diff --git a/packages/svelte/tests/css/samples/global-block/_config.js b/packages/svelte/tests/css/samples/global-block/_config.js
index bee0d7204d00..18a56e9a97d1 100644
--- a/packages/svelte/tests/css/samples/global-block/_config.js
+++ b/packages/svelte/tests/css/samples/global-block/_config.js
@@ -7,14 +7,28 @@ export default test({
code: 'css_unused_selector',
message: 'Unused CSS selector ".unused :global"',
start: {
- line: 69,
+ line: 73,
column: 1,
- character: 917
+ character: 964
},
end: {
- line: 69,
+ line: 73,
column: 16,
- character: 932
+ character: 979
+ }
+ },
+ {
+ code: 'css_unused_selector',
+ message: 'Unused CSS selector "unused :global"',
+ start: {
+ line: 104,
+ column: 29,
+ character: 1270
+ },
+ end: {
+ line: 104,
+ column: 43,
+ character: 1284
}
}
]
diff --git a/packages/svelte/tests/css/samples/global-block/expected.css b/packages/svelte/tests/css/samples/global-block/expected.css
index 438749224ba6..be1838fd98e3 100644
--- a/packages/svelte/tests/css/samples/global-block/expected.css
+++ b/packages/svelte/tests/css/samples/global-block/expected.css
@@ -3,6 +3,10 @@
.x {
color: green;
}
+
+ .a, .selector, .list {
+ color: green;
+ }
/*}*/
div.svelte-xyz {
@@ -90,3 +94,13 @@
opacity: 1;
}
}
+
+ x, y {
+ color: green;
+ }
+
+ div.svelte-xyz, div.svelte-xyz y /* (unused) unused*/ {
+ z {
+ color: green;
+ }
+ }
diff --git a/packages/svelte/tests/css/samples/global-block/input.svelte b/packages/svelte/tests/css/samples/global-block/input.svelte
index a1833636a13f..86d438031a96 100644
--- a/packages/svelte/tests/css/samples/global-block/input.svelte
+++ b/packages/svelte/tests/css/samples/global-block/input.svelte
@@ -5,6 +5,10 @@
.x {
color: green;
}
+
+ .a, .selector, .list {
+ color: green;
+ }
}
div :global {
@@ -92,4 +96,14 @@
opacity: 1;
}
}
+
+ :global x, :global y {
+ color: green;
+ }
+
+ div :global, div :global y, unused :global {
+ z {
+ color: green;
+ }
+ }
diff --git a/packages/svelte/tests/css/samples/global-keyframes/_config.js b/packages/svelte/tests/css/samples/global-keyframes/_config.js
new file mode 100644
index 000000000000..30953854ad59
--- /dev/null
+++ b/packages/svelte/tests/css/samples/global-keyframes/_config.js
@@ -0,0 +1,5 @@
+import { test } from '../../test';
+
+export default test({
+ hasGlobal: true
+});
diff --git a/packages/svelte/tests/css/samples/global-local-nested/_config.js b/packages/svelte/tests/css/samples/global-local-nested/_config.js
new file mode 100644
index 000000000000..5a7796ebac7e
--- /dev/null
+++ b/packages/svelte/tests/css/samples/global-local-nested/_config.js
@@ -0,0 +1,5 @@
+import { test } from '../../test';
+
+export default test({
+ hasGlobal: false
+});
diff --git a/packages/svelte/tests/css/samples/global-local-nested/expected.css b/packages/svelte/tests/css/samples/global-local-nested/expected.css
new file mode 100644
index 000000000000..8eadf2b948c4
--- /dev/null
+++ b/packages/svelte/tests/css/samples/global-local-nested/expected.css
@@ -0,0 +1,12 @@
+
+ div.svelte-xyz {
+ .whatever {
+ color: green;
+ }
+ }
+
+ .whatever {
+ div.svelte-xyz {
+ color: green;
+ }
+ }
diff --git a/packages/svelte/tests/css/samples/global-local-nested/input.svelte b/packages/svelte/tests/css/samples/global-local-nested/input.svelte
new file mode 100644
index 000000000000..60210be75363
--- /dev/null
+++ b/packages/svelte/tests/css/samples/global-local-nested/input.svelte
@@ -0,0 +1,15 @@
+{@html whatever}
+
+
diff --git a/packages/svelte/tests/css/samples/global-local/_config.js b/packages/svelte/tests/css/samples/global-local/_config.js
new file mode 100644
index 000000000000..5a7796ebac7e
--- /dev/null
+++ b/packages/svelte/tests/css/samples/global-local/_config.js
@@ -0,0 +1,5 @@
+import { test } from '../../test';
+
+export default test({
+ hasGlobal: false
+});
diff --git a/packages/svelte/tests/css/samples/global-local/expected.css b/packages/svelte/tests/css/samples/global-local/expected.css
new file mode 100644
index 000000000000..c4fc74fb1aaf
--- /dev/null
+++ b/packages/svelte/tests/css/samples/global-local/expected.css
@@ -0,0 +1,8 @@
+
+ div.svelte-xyz .whatever {
+ color: green;
+ }
+
+ .whatever div.svelte-xyz {
+ color: green;
+ }
diff --git a/packages/svelte/tests/css/samples/global-local/input.svelte b/packages/svelte/tests/css/samples/global-local/input.svelte
new file mode 100644
index 000000000000..bff97ab485a2
--- /dev/null
+++ b/packages/svelte/tests/css/samples/global-local/input.svelte
@@ -0,0 +1,11 @@
+{@html whatever}
+
+
diff --git a/packages/svelte/tests/css/samples/global-with-nesting/_config.js b/packages/svelte/tests/css/samples/global-with-nesting/_config.js
index 292c6c49ac9d..6cec7c236045 100644
--- a/packages/svelte/tests/css/samples/global-with-nesting/_config.js
+++ b/packages/svelte/tests/css/samples/global-with-nesting/_config.js
@@ -1,5 +1,7 @@
import { test } from '../../test';
export default test({
- warnings: []
+ warnings: [],
+
+ hasGlobal: false
});
diff --git a/packages/svelte/tests/css/samples/global-with-nesting/expected.css b/packages/svelte/tests/css/samples/global-with-nesting/expected.css
index dcb8a0e48195..1863c57d853a 100644
--- a/packages/svelte/tests/css/samples/global-with-nesting/expected.css
+++ b/packages/svelte/tests/css/samples/global-with-nesting/expected.css
@@ -1,5 +1,10 @@
div.svelte-xyz {
&.class{
- color: red;
+ color: green;
+ }
+ }
+ * {
+ &:hover .class.svelte-xyz {
+ color: green;
}
}
\ No newline at end of file
diff --git a/packages/svelte/tests/css/samples/global-with-nesting/input.svelte b/packages/svelte/tests/css/samples/global-with-nesting/input.svelte
index 0c73ed7a78a2..2c1d2b5ebdac 100644
--- a/packages/svelte/tests/css/samples/global-with-nesting/input.svelte
+++ b/packages/svelte/tests/css/samples/global-with-nesting/input.svelte
@@ -1,7 +1,12 @@
diff --git a/packages/svelte/tests/css/samples/global/_config.js b/packages/svelte/tests/css/samples/global/_config.js
new file mode 100644
index 000000000000..30953854ad59
--- /dev/null
+++ b/packages/svelte/tests/css/samples/global/_config.js
@@ -0,0 +1,5 @@
+import { test } from '../../test';
+
+export default test({
+ hasGlobal: true
+});
diff --git a/packages/svelte/tests/css/samples/has/_config.js b/packages/svelte/tests/css/samples/has/_config.js
index 8d89d98cbdb0..5700a09b9627 100644
--- a/packages/svelte/tests/css/samples/has/_config.js
+++ b/packages/svelte/tests/css/samples/has/_config.js
@@ -6,210 +6,238 @@ export default test({
code: 'css_unused_selector',
message: 'Unused CSS selector ".unused:has(y)"',
start: {
- line: 33,
+ line: 41,
column: 1,
- character: 330
+ character: 378
},
end: {
- line: 33,
+ line: 41,
column: 15,
- character: 344
+ character: 392
}
},
{
code: 'css_unused_selector',
message: 'Unused CSS selector ".unused:has(:global(y))"',
start: {
- line: 36,
+ line: 44,
column: 1,
- character: 365
+ character: 413
},
end: {
- line: 36,
+ line: 44,
column: 24,
- character: 388
+ character: 436
}
},
{
code: 'css_unused_selector',
message: 'Unused CSS selector "x:has(.unused)"',
start: {
- line: 39,
+ line: 47,
column: 1,
- character: 409
+ character: 457
},
end: {
- line: 39,
+ line: 47,
column: 15,
- character: 423
+ character: 471
}
},
{
code: 'css_unused_selector',
message: 'Unused CSS selector ":global(.foo):has(.unused)"',
start: {
- line: 42,
+ line: 50,
column: 1,
- character: 444
+ character: 492
},
end: {
- line: 42,
+ line: 50,
column: 27,
- character: 470
+ character: 518
}
},
{
code: 'css_unused_selector',
message: 'Unused CSS selector "x:has(y):has(.unused)"',
start: {
- line: 52,
+ line: 60,
column: 1,
- character: 578
+ character: 626
},
end: {
- line: 52,
+ line: 60,
column: 22,
- character: 599
+ character: 647
}
},
{
code: 'css_unused_selector',
message: 'Unused CSS selector ".unused"',
start: {
- line: 71,
+ line: 79,
column: 2,
- character: 804
+ character: 852
},
end: {
- line: 71,
+ line: 79,
column: 9,
- character: 811
+ character: 859
}
},
{
code: 'css_unused_selector',
message: 'Unused CSS selector ".unused x:has(y)"',
start: {
- line: 87,
+ line: 95,
column: 1,
- character: 958
+ character: 1006
},
end: {
- line: 87,
+ line: 95,
column: 17,
- character: 974
+ character: 1022
}
},
{
code: 'css_unused_selector',
message: 'Unused CSS selector ".unused:has(.unused)"',
start: {
- line: 90,
+ line: 98,
column: 1,
- character: 995
+ character: 1043
},
end: {
- line: 90,
+ line: 98,
column: 21,
- character: 1015
+ character: 1063
}
},
{
code: 'css_unused_selector',
message: 'Unused CSS selector "x:has(> z)"',
start: {
- line: 100,
+ line: 108,
column: 1,
- character: 1115
+ character: 1163
},
end: {
- line: 100,
+ line: 108,
column: 11,
- character: 1125
+ character: 1173
}
},
{
code: 'css_unused_selector',
message: 'Unused CSS selector "x:has(> d)"',
start: {
- line: 103,
+ line: 111,
column: 1,
- character: 1146
+ character: 1194
},
end: {
- line: 103,
+ line: 111,
column: 11,
- character: 1156
+ character: 1204
}
},
{
code: 'css_unused_selector',
message: 'Unused CSS selector "x:has(~ y)"',
start: {
- line: 123,
+ line: 131,
column: 1,
- character: 1348
+ character: 1396
},
end: {
- line: 123,
+ line: 131,
column: 11,
- character: 1358
+ character: 1406
+ }
+ },
+ {
+ code: 'css_unused_selector',
+ message: 'Unused CSS selector "d:has(+ f)"',
+ start: {
+ line: 141,
+ column: 1,
+ character: 1494
+ },
+ end: {
+ line: 141,
+ column: 11,
+ character: 1504
}
},
{
code: 'css_unused_selector',
message: 'Unused CSS selector "f:has(~ d)"',
start: {
- line: 133,
+ line: 144,
column: 1,
- character: 1446
+ character: 1525
},
end: {
- line: 133,
+ line: 144,
column: 11,
- character: 1456
+ character: 1535
}
},
{
code: 'css_unused_selector',
message: 'Unused CSS selector ":has(.unused)"',
start: {
- line: 141,
+ line: 152,
column: 2,
- character: 1529
+ character: 1608
},
end: {
- line: 141,
+ line: 152,
column: 15,
- character: 1542
+ character: 1621
}
},
{
code: 'css_unused_selector',
message: 'Unused CSS selector "&:has(.unused)"',
start: {
- line: 147,
+ line: 158,
column: 2,
- character: 1600
+ character: 1679
},
end: {
- line: 147,
+ line: 158,
column: 16,
- character: 1614
+ character: 1693
}
},
{
code: 'css_unused_selector',
message: 'Unused CSS selector ":global(.foo):has(.unused)"',
start: {
- line: 155,
+ line: 166,
column: 1,
- character: 1684
+ character: 1763
},
end: {
- line: 155,
+ line: 166,
column: 27,
- character: 1710
+ character: 1789
+ }
+ },
+ {
+ code: 'css_unused_selector',
+ message: 'Unused CSS selector "h:has(> h > i)"',
+ start: {
+ line: 173,
+ column: 1,
+ character: 1848
+ },
+ end: {
+ line: 173,
+ column: 15,
+ character: 1862
}
}
]
diff --git a/packages/svelte/tests/css/samples/has/expected.css b/packages/svelte/tests/css/samples/has/expected.css
index b257370d61f3..2ce4d2bec5cd 100644
--- a/packages/svelte/tests/css/samples/has/expected.css
+++ b/packages/svelte/tests/css/samples/has/expected.css
@@ -118,6 +118,9 @@
d.svelte-xyz:has(~ f:where(.svelte-xyz)) {
color: green;
}
+ /* (unused) d:has(+ f) {
+ color: red;
+ }*/
/* (unused) f:has(~ d) {
color: red;
}*/
@@ -143,3 +146,13 @@
/* (unused) :global(.foo):has(.unused) {
color: red;
}*/
+
+ g.svelte-xyz:has(> h:where(.svelte-xyz) > i:where(.svelte-xyz)) {
+ color: green;
+ }
+ /* (unused) h:has(> h > i) {
+ color: red;
+ }*/
+ g.svelte-xyz:has(+ j:where(.svelte-xyz) > k:where(.svelte-xyz)) {
+ color: green;
+ }
\ No newline at end of file
diff --git a/packages/svelte/tests/css/samples/has/input.svelte b/packages/svelte/tests/css/samples/has/input.svelte
index 9b254996bf30..033471bc1696 100644
--- a/packages/svelte/tests/css/samples/has/input.svelte
+++ b/packages/svelte/tests/css/samples/has/input.svelte
@@ -9,6 +9,14 @@
+
+
+
+
+
+
+
+
diff --git a/packages/svelte/tests/css/samples/render-tag-loop/_config.js b/packages/svelte/tests/css/samples/render-tag-loop/_config.js
index f623b92cc38b..292c6c49ac9d 100644
--- a/packages/svelte/tests/css/samples/render-tag-loop/_config.js
+++ b/packages/svelte/tests/css/samples/render-tag-loop/_config.js
@@ -1,20 +1,5 @@
import { test } from '../../test';
export default test({
- warnings: [
- {
- code: 'css_unused_selector',
- message: 'Unused CSS selector "div + div"',
- start: {
- line: 19,
- column: 1,
- character: 185
- },
- end: {
- line: 19,
- column: 10,
- character: 194
- }
- }
- ]
+ warnings: []
});
diff --git a/packages/svelte/tests/css/samples/render-tag-loop/expected.css b/packages/svelte/tests/css/samples/render-tag-loop/expected.css
index 9ced15e96407..3e449286c997 100644
--- a/packages/svelte/tests/css/samples/render-tag-loop/expected.css
+++ b/packages/svelte/tests/css/samples/render-tag-loop/expected.css
@@ -2,9 +2,12 @@
div.svelte-xyz div:where(.svelte-xyz) {
color: green;
}
- /* (unused) div + div {
- color: red; /* this is marked as unused, but only because we've written an infinite loop - worth fixing? *\/
- }*/
+ div.svelte-xyz + div:where(.svelte-xyz) {
+ color: green;
+ }
div.svelte-xyz:has(div:where(.svelte-xyz)) {
color: green;
}
+ span.svelte-xyz:has(~span:where(.svelte-xyz)) {
+ color: green;
+ }
diff --git a/packages/svelte/tests/css/samples/render-tag-loop/input.svelte b/packages/svelte/tests/css/samples/render-tag-loop/input.svelte
index ade8df574489..3c55261f1845 100644
--- a/packages/svelte/tests/css/samples/render-tag-loop/input.svelte
+++ b/packages/svelte/tests/css/samples/render-tag-loop/input.svelte
@@ -12,14 +12,22 @@
{/snippet}
+{#snippet c()}
+
+ {@render c()}
+{/snippet}
+
diff --git a/packages/svelte/tests/css/samples/siblings-combinator-component/Child.svelte b/packages/svelte/tests/css/samples/siblings-combinator-component/Child.svelte
new file mode 100644
index 000000000000..1df9f35e50be
--- /dev/null
+++ b/packages/svelte/tests/css/samples/siblings-combinator-component/Child.svelte
@@ -0,0 +1,5 @@
+
+
+{@render foo()}
diff --git a/packages/svelte/tests/css/samples/siblings-combinator-component/_config.js b/packages/svelte/tests/css/samples/siblings-combinator-component/_config.js
new file mode 100644
index 000000000000..837fa20ae104
--- /dev/null
+++ b/packages/svelte/tests/css/samples/siblings-combinator-component/_config.js
@@ -0,0 +1,20 @@
+import { test } from '../../test';
+
+export default test({
+ warnings: [
+ {
+ code: 'css_unused_selector',
+ message: 'Unused CSS selector "n + m"',
+ end: {
+ character: 468,
+ column: 6,
+ line: 36
+ },
+ start: {
+ character: 463,
+ column: 1,
+ line: 36
+ }
+ }
+ ]
+});
diff --git a/packages/svelte/tests/css/samples/siblings-combinator-component/expected.css b/packages/svelte/tests/css/samples/siblings-combinator-component/expected.css
new file mode 100644
index 000000000000..d2657ccd21df
--- /dev/null
+++ b/packages/svelte/tests/css/samples/siblings-combinator-component/expected.css
@@ -0,0 +1,8 @@
+ x.svelte-xyz + y:where(.svelte-xyz) { color: green; }
+ x.svelte-xyz + v:where(.svelte-xyz) { color: green; }
+ x.svelte-xyz + z:where(.svelte-xyz) { color: green; }
+ y.svelte-xyz + z:where(.svelte-xyz) { color: green; }
+ v.svelte-xyz + z:where(.svelte-xyz) { color: green; }
+ .component + z.svelte-xyz { color: green; }
+
+ /* (unused) n + m { color: red; }*/
diff --git a/packages/svelte/tests/css/samples/siblings-combinator-component/input.svelte b/packages/svelte/tests/css/samples/siblings-combinator-component/input.svelte
new file mode 100644
index 000000000000..8d80acffb364
--- /dev/null
+++ b/packages/svelte/tests/css/samples/siblings-combinator-component/input.svelte
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+ {#snippet foo()}
+
+ {/snippet}
+
+
+
+
+
+
+
+ {#snippet foo()}
+
+
+
+ {/snippet}
+
+
+
+
+
diff --git a/packages/svelte/tests/css/samples/siblings-combinator-slot/_config.js b/packages/svelte/tests/css/samples/siblings-combinator-slot/_config.js
index 2786baeff80b..8a8f561d014b 100644
--- a/packages/svelte/tests/css/samples/siblings-combinator-slot/_config.js
+++ b/packages/svelte/tests/css/samples/siblings-combinator-slot/_config.js
@@ -5,14 +5,8 @@ export default test({
{
code: 'css_unused_selector',
message: 'Unused CSS selector ".b + .c"',
- start: { character: 110, column: 1, line: 10 },
- end: { character: 117, column: 8, line: 10 }
- },
- {
- code: 'css_unused_selector',
- message: 'Unused CSS selector ".c + .f"',
- start: { character: 135, column: 1, line: 11 },
- end: { character: 142, column: 8, line: 11 }
+ start: { character: 137, column: 1, line: 11 },
+ end: { character: 144, column: 8, line: 11 }
}
]
});
diff --git a/packages/svelte/tests/css/samples/siblings-combinator-slot/expected.css b/packages/svelte/tests/css/samples/siblings-combinator-slot/expected.css
index 643f6cf13f77..85cbb77e6514 100644
--- a/packages/svelte/tests/css/samples/siblings-combinator-slot/expected.css
+++ b/packages/svelte/tests/css/samples/siblings-combinator-slot/expected.css
@@ -1,7 +1,7 @@
.d.svelte-xyz + .e:where(.svelte-xyz) { color: green; }
.a.svelte-xyz + .b:where(.svelte-xyz) { color: green; }
+ .c.svelte-xyz + .f:where(.svelte-xyz) { color: green; }
/* no match */
/* (unused) .b + .c { color: red; }*/
- /* (unused) .c + .f { color: red; }*/
diff --git a/packages/svelte/tests/css/samples/siblings-combinator-slot/input.svelte b/packages/svelte/tests/css/samples/siblings-combinator-slot/input.svelte
index 1b543f97b713..57e1df1507be 100644
--- a/packages/svelte/tests/css/samples/siblings-combinator-slot/input.svelte
+++ b/packages/svelte/tests/css/samples/siblings-combinator-slot/input.svelte
@@ -5,10 +5,10 @@
diff --git a/packages/svelte/tests/css/samples/undefined-with-scope/expected.html b/packages/svelte/tests/css/samples/undefined-with-scope/expected.html
index 5eecaa9bb256..5548be6e5aa5 100644
--- a/packages/svelte/tests/css/samples/undefined-with-scope/expected.html
+++ b/packages/svelte/tests/css/samples/undefined-with-scope/expected.html
@@ -1 +1,2 @@
-Foo
\ No newline at end of file
+Foo
+Bar
\ No newline at end of file
diff --git a/packages/svelte/tests/css/samples/undefined-with-scope/input.svelte b/packages/svelte/tests/css/samples/undefined-with-scope/input.svelte
index c68fb40dea09..20639600d02e 100644
--- a/packages/svelte/tests/css/samples/undefined-with-scope/input.svelte
+++ b/packages/svelte/tests/css/samples/undefined-with-scope/input.svelte
@@ -1,3 +1,4 @@
-Foo
\ No newline at end of file
+Foo
+Bar
diff --git a/packages/svelte/tests/css/samples/unicode-identifier/_config.js b/packages/svelte/tests/css/samples/unicode-identifier/_config.js
new file mode 100644
index 000000000000..da4ea75e36b6
--- /dev/null
+++ b/packages/svelte/tests/css/samples/unicode-identifier/_config.js
@@ -0,0 +1,76 @@
+import { test } from '../../test';
+
+export default test({
+ warnings: [
+ {
+ code: 'css_unused_selector',
+ message: 'Unused CSS selector ".\\61 sdf"',
+ start: {
+ line: 22,
+ column: 1,
+ character: 465
+ },
+ end: {
+ line: 22,
+ column: 10,
+ character: 474
+ }
+ },
+ {
+ code: 'css_unused_selector',
+ message: 'Unused CSS selector ".\\61\n\tsdf"',
+ start: {
+ line: 23,
+ column: 1,
+ character: 492
+ },
+ end: {
+ line: 24,
+ column: 4,
+ character: 501
+ }
+ },
+ {
+ code: 'css_unused_selector',
+ message: 'Unused CSS selector ".\\61\n sdf"',
+ start: {
+ line: 25,
+ column: 1,
+ character: 519
+ },
+ end: {
+ line: 26,
+ column: 4,
+ character: 528
+ }
+ },
+ {
+ code: 'css_unused_selector',
+ message: 'Unused CSS selector "#\\31span"',
+ start: {
+ line: 28,
+ column: 1,
+ character: 547
+ },
+ end: {
+ line: 28,
+ column: 9,
+ character: 555
+ }
+ },
+ {
+ code: 'css_unused_selector',
+ message: 'Unused CSS selector "#\\31 span"',
+ start: {
+ line: 29,
+ column: 1,
+ character: 573
+ },
+ end: {
+ line: 29,
+ column: 10,
+ character: 582
+ }
+ }
+ ]
+});
diff --git a/packages/svelte/tests/css/samples/unicode-identifier/expected.css b/packages/svelte/tests/css/samples/unicode-identifier/expected.css
new file mode 100644
index 000000000000..fb8b936deed2
--- /dev/null
+++ b/packages/svelte/tests/css/samples/unicode-identifier/expected.css
@@ -0,0 +1,21 @@
+ #\31\32\33 .svelte-xyz{ color: green; }
+ #\31 23.svelte-xyz { color: green; }
+ #line\a break.svelte-xyz { color: green; }
+ #line\a
+break.svelte-xyz { color: green; }
+ #line\00000abreak.svelte-xyz { color: green; }
+ #line\00000a break.svelte-xyz { color: green; }
+ #line\00000a break.svelte-xyz { color: green; }
+ .a\1f642 b.svelte-xyz { color: green; }
+
+ .\61sdf.svelte-xyz { color: green; }
+
+ /* (unused) .\61 sdf { color: red; }*/
+ /* (unused) .\61
+ sdf { color: red; }*/
+ /* (unused) .\61
+ sdf { color: red; }*/
+
+ /* (unused) #\31span { color: red; }*/
+ /* (unused) #\31 span { color: red; }*/
+ #\31 .svelte-xyz span:where(.svelte-xyz) { color: green; }
\ No newline at end of file
diff --git a/packages/svelte/tests/css/samples/unicode-identifier/expected.html b/packages/svelte/tests/css/samples/unicode-identifier/expected.html
new file mode 100644
index 000000000000..9b5f8ede4730
--- /dev/null
+++ b/packages/svelte/tests/css/samples/unicode-identifier/expected.html
@@ -0,0 +1,7 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/packages/svelte/tests/css/samples/unicode-identifier/input.svelte b/packages/svelte/tests/css/samples/unicode-identifier/input.svelte
new file mode 100644
index 000000000000..04676c4c4db2
--- /dev/null
+++ b/packages/svelte/tests/css/samples/unicode-identifier/input.svelte
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/packages/svelte/tests/css/samples/view-transition/expected.css b/packages/svelte/tests/css/samples/view-transition/expected.css
index afc84d52ebf5..e216a4d3ad9b 100644
--- a/packages/svelte/tests/css/samples/view-transition/expected.css
+++ b/packages/svelte/tests/css/samples/view-transition/expected.css
@@ -8,9 +8,15 @@
::view-transition-old {
animation-duration: 0.5s;
}
+ ::view-transition-old:only-child {
+ animation-duration: 0.5s;
+ }
::view-transition-new {
animation-duration: 0.5s;
}
+ ::view-transition-new:only-child {
+ animation-duration: 0.5s;
+ }
::view-transition-image-pair {
animation-duration: 0.5s;
}
diff --git a/packages/svelte/tests/css/samples/view-transition/input.svelte b/packages/svelte/tests/css/samples/view-transition/input.svelte
index ebb2b3fd88e0..345213ccd3f5 100644
--- a/packages/svelte/tests/css/samples/view-transition/input.svelte
+++ b/packages/svelte/tests/css/samples/view-transition/input.svelte
@@ -8,9 +8,15 @@
::view-transition-old {
animation-duration: 0.5s;
}
+ ::view-transition-old:only-child {
+ animation-duration: 0.5s;
+ }
::view-transition-new {
animation-duration: 0.5s;
}
+ ::view-transition-new:only-child {
+ animation-duration: 0.5s;
+ }
::view-transition-image-pair {
animation-duration: 0.5s;
}
diff --git a/packages/svelte/tests/css/test.ts b/packages/svelte/tests/css/test.ts
index dd51f52eabc3..8846b1d9862e 100644
--- a/packages/svelte/tests/css/test.ts
+++ b/packages/svelte/tests/css/test.ts
@@ -34,6 +34,7 @@ interface CssTest extends BaseTest {
compileOptions?: Partial;
warnings?: Warning[];
props?: Record;
+ hasGlobal?: boolean;
}
/**
@@ -78,6 +79,14 @@ const { test, run } = suite(async (config, cwd) => {
// assert_html_equal(actual_ssr, expected.html);
}
+ if (config.hasGlobal !== undefined) {
+ const metadata = JSON.parse(
+ fs.readFileSync(`${cwd}/_output/client/input.svelte.css.json`, 'utf-8')
+ );
+
+ assert.equal(metadata.hasGlobal, config.hasGlobal);
+ }
+
const dom_css = fs.readFileSync(`${cwd}/_output/client/input.svelte.css`, 'utf-8').trim();
const ssr_css = fs.readFileSync(`${cwd}/_output/server/input.svelte.css`, 'utf-8').trim();
diff --git a/packages/svelte/tests/helpers.js b/packages/svelte/tests/helpers.js
index 87bcb473e7e2..2d825dbb7cbf 100644
--- a/packages/svelte/tests/helpers.js
+++ b/packages/svelte/tests/helpers.js
@@ -146,6 +146,10 @@ export async function compile_directory(
if (compiled.css) {
write(`${output_dir}/${file}.css`, compiled.css.code);
+ write(
+ `${output_dir}/${file}.css.json`,
+ JSON.stringify({ hasGlobal: compiled.css.hasGlobal })
+ );
if (output_map) {
write(`${output_dir}/${file}.css.map`, JSON.stringify(compiled.css.map, null, '\t'));
}
@@ -186,3 +190,46 @@ if (typeof window !== 'undefined') {
};
});
}
+
+export const fragments = /** @type {'html' | 'tree'} */ (process.env.FRAGMENTS) ?? 'html';
+
+/**
+ * @param {any[]} logs
+ */
+export function normalise_trace_logs(logs) {
+ let normalised = [];
+
+ logs = logs.slice();
+
+ while (logs.length > 0) {
+ const log = logs.shift();
+
+ if (log instanceof Error) {
+ continue;
+ }
+
+ if (typeof log === 'string' && log.includes('%c')) {
+ const split = log.split('%c');
+
+ const first = /** @type {string} */ (split.shift()).trim();
+ if (first) normalised.push({ log: first });
+
+ while (split.length > 0) {
+ const log = /** @type {string} */ (split.shift()).trim();
+ const highlighted = logs.shift() === 'color: CornflowerBlue; font-weight: bold';
+
+ // omit timings, as they will differ between runs
+ if (/\(.+ms\)/.test(log)) continue;
+
+ normalised.push({
+ log,
+ highlighted
+ });
+ }
+ } else {
+ normalised.push({ log });
+ }
+ }
+
+ return normalised;
+}
diff --git a/packages/svelte/tests/html_equal.js b/packages/svelte/tests/html_equal.js
index 0ebf1fa6bd53..b637e4d53874 100644
--- a/packages/svelte/tests/html_equal.js
+++ b/packages/svelte/tests/html_equal.js
@@ -1,8 +1,21 @@
+import { COMMENT_NODE, ELEMENT_NODE, TEXT_NODE } from '#client/constants';
import { assert } from 'vitest';
-/** @param {Element} node */
-function clean_children(node) {
+/**
+ * @param {Element} node
+ * @param {{ preserveComments: boolean }} opts
+ */
+function clean_children(node, opts) {
let previous = null;
+ let has_element_children = false;
+ let template =
+ node.nodeName === 'TEMPLATE' ? /** @type {HTMLTemplateElement} */ (node) : undefined;
+
+ if (template) {
+ const div = document.createElement('div');
+ div.append(template.content);
+ node = div;
+ }
// sort attributes
const attributes = Array.from(node.attributes).sort((a, b) => {
@@ -14,11 +27,16 @@ function clean_children(node) {
});
attributes.forEach((attr) => {
+ // Strip out the special onload/onerror hydration events from the test output
+ if ((attr.name === 'onload' || attr.name === 'onerror') && attr.value === 'this.__e=event') {
+ return;
+ }
+
node.setAttribute(attr.name, attr.value);
});
for (let child of [...node.childNodes]) {
- if (child.nodeType === 3) {
+ if (child.nodeType === TEXT_NODE) {
let text = /** @type {Text} */ (child);
if (
@@ -27,66 +45,84 @@ function clean_children(node) {
node.tagName !== 'tspan'
) {
node.removeChild(child);
+ continue;
}
- text.data = text.data.replace(/[ \t\n\r\f]+/g, '\n');
+ text.data = text.data.replace(/[^\S]+/g, ' ');
- if (previous && previous.nodeType === 3) {
+ if (previous && previous.nodeType === TEXT_NODE) {
const prev = /** @type {Text} */ (previous);
prev.data += text.data;
- prev.data = prev.data.replace(/[ \t\n\r\f]+/g, '\n');
-
node.removeChild(text);
+
text = prev;
+ text.data = text.data.replace(/[^\S]+/g, ' ');
+
+ continue;
}
- } else if (child.nodeType === 8) {
+ }
+
+ if (child.nodeType === COMMENT_NODE && !opts.preserveComments) {
// comment
- // do nothing
- } else {
- clean_children(/** @type {Element} */ (child));
+ child.remove();
+ continue;
+ }
+
+ // add newlines for better readability and potentially recurse into children
+ if (child.nodeType === ELEMENT_NODE || child.nodeType === COMMENT_NODE) {
+ if (previous?.nodeType === TEXT_NODE) {
+ const prev = /** @type {Text} */ (previous);
+ prev.data = prev.data.replace(/^[^\S]+$/, '\n');
+ } else if (previous?.nodeType === ELEMENT_NODE || previous?.nodeType === COMMENT_NODE) {
+ node.insertBefore(document.createTextNode('\n'), child);
+ }
+
+ if (child.nodeType === ELEMENT_NODE) {
+ has_element_children = true;
+ clean_children(/** @type {Element} */ (child), opts);
+ }
}
previous = child;
}
// collapse whitespace
- if (node.firstChild && node.firstChild.nodeType === 3) {
+ if (node.firstChild && node.firstChild.nodeType === TEXT_NODE) {
const text = /** @type {Text} */ (node.firstChild);
- text.data = text.data.replace(/^[ \t\n\r\f]+/, '');
- if (!text.data.length) node.removeChild(text);
+ text.data = text.data.trimStart();
}
- if (node.lastChild && node.lastChild.nodeType === 3) {
+ if (node.lastChild && node.lastChild.nodeType === TEXT_NODE) {
const text = /** @type {Text} */ (node.lastChild);
- text.data = text.data.replace(/[ \t\n\r\f]+$/, '');
- if (!text.data.length) node.removeChild(text);
+ text.data = text.data.trimEnd();
+ }
+
+ // indent code for better readability
+ if (has_element_children && node.parentNode) {
+ node.innerHTML = `\n\ ${node.innerHTML.replace(/\n/g, '\n ')}\n`;
+ }
+
+ if (template) {
+ template.innerHTML = node.innerHTML;
}
}
/**
* @param {Window} window
* @param {string} html
- * @param {{ removeDataSvelte?: boolean, preserveComments?: boolean }} param2
+ * @param {{ preserveComments?: boolean }} opts
*/
-export function normalize_html(
- window,
- html,
- { removeDataSvelte = false, preserveComments = false }
-) {
+export function normalize_html(window, html, { preserveComments = false } = {}) {
try {
const node = window.document.createElement('div');
- node.innerHTML = html
- .replace(/()/g, preserveComments ? '$1' : '')
- .replace(/(data-svelte-h="[^"]+")/g, removeDataSvelte ? '' : '$1')
- .replace(/>[ \t\n\r\f]+<')
- // Strip out the special onload/onerror hydration events from the test output
- .replace(/\s?onerror="this.__e=event"|\s?onload="this.__e=event"/g, '')
- .trim();
- clean_children(node);
+
+ node.innerHTML = html.trim();
+ clean_children(node, { preserveComments });
+
return node.innerHTML;
} catch (err) {
- throw new Error(`Failed to normalize HTML:\n${html}`);
+ throw new Error(`Failed to normalize HTML:\n${html}\nCause: ${err}`);
}
}
@@ -99,67 +135,52 @@ export function normalize_new_line(html) {
}
/**
- * @param {{ removeDataSvelte?: boolean }} options
+ * @param {string} actual
+ * @param {string} expected
+ * @param {string} [message]
*/
-export function setup_html_equal(options = {}) {
- /**
- * @param {string} actual
- * @param {string} expected
- * @param {string} [message]
- */
- const assert_html_equal = (actual, expected, message) => {
- try {
- assert.deepEqual(
- normalize_html(window, actual, options),
- normalize_html(window, expected, options),
- message
- );
- } catch (e) {
- if (Error.captureStackTrace)
- Error.captureStackTrace(/** @type {Error} */ (e), assert_html_equal);
- throw e;
- }
- };
-
- /**
- *
- * @param {string} actual
- * @param {string} expected
- * @param {{ preserveComments?: boolean, withoutNormalizeHtml?: boolean }} param2
- * @param {string} [message]
- */
- const assert_html_equal_with_options = (
- actual,
- expected,
- { preserveComments, withoutNormalizeHtml },
- message
- ) => {
- try {
- assert.deepEqual(
- withoutNormalizeHtml
- ? normalize_new_line(actual.trim())
- .replace(/(\sdata-svelte-h="[^"]+")/g, options.removeDataSvelte ? '' : '$1')
- .replace(/()/g, preserveComments !== false ? '$1' : '')
- : normalize_html(window, actual.trim(), { ...options, preserveComments }),
- withoutNormalizeHtml
- ? normalize_new_line(expected.trim())
- .replace(/(\sdata-svelte-h="[^"]+")/g, options.removeDataSvelte ? '' : '$1')
- .replace(/()/g, preserveComments !== false ? '$1' : '')
- : normalize_html(window, expected.trim(), { ...options, preserveComments }),
- message
- );
- } catch (e) {
- if (Error.captureStackTrace)
- Error.captureStackTrace(/** @type {Error} */ (e), assert_html_equal_with_options);
- throw e;
- }
- };
-
- return {
- assert_html_equal,
- assert_html_equal_with_options
- };
-}
+export const assert_html_equal = (actual, expected, message) => {
+ try {
+ assert.deepEqual(normalize_html(window, actual), normalize_html(window, expected), message);
+ } catch (e) {
+ if (Error.captureStackTrace)
+ Error.captureStackTrace(/** @type {Error} */ (e), assert_html_equal);
+ throw e;
+ }
+};
-// Common case without options
-export const { assert_html_equal, assert_html_equal_with_options } = setup_html_equal();
+/**
+ *
+ * @param {string} actual
+ * @param {string} expected
+ * @param {{ preserveComments?: boolean, withoutNormalizeHtml?: boolean }} param2
+ * @param {string} [message]
+ */
+export const assert_html_equal_with_options = (
+ actual,
+ expected,
+ { preserveComments, withoutNormalizeHtml },
+ message
+) => {
+ try {
+ assert.deepEqual(
+ withoutNormalizeHtml
+ ? normalize_new_line(actual.trim()).replace(
+ /()/g,
+ preserveComments !== false ? '$1' : ''
+ )
+ : normalize_html(window, actual.trim(), { preserveComments }),
+ withoutNormalizeHtml
+ ? normalize_new_line(expected.trim()).replace(
+ /()/g,
+ preserveComments !== false ? '$1' : ''
+ )
+ : normalize_html(window, expected.trim(), { preserveComments }),
+ message
+ );
+ } catch (e) {
+ if (Error.captureStackTrace)
+ Error.captureStackTrace(/** @type {Error} */ (e), assert_html_equal_with_options);
+ throw e;
+ }
+};
diff --git a/packages/svelte/tests/hydration/samples/cloudflare-mirage-borking-2/_config.js b/packages/svelte/tests/hydration/samples/cloudflare-mirage-borking-2/_config.js
new file mode 100644
index 000000000000..56ba73b06408
--- /dev/null
+++ b/packages/svelte/tests/hydration/samples/cloudflare-mirage-borking-2/_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-2/_expected.html b/packages/svelte/tests/hydration/samples/cloudflare-mirage-borking-2/_expected.html
new file mode 100644
index 000000000000..5179fb04a5f7
--- /dev/null
+++ b/packages/svelte/tests/hydration/samples/cloudflare-mirage-borking-2/_expected.html
@@ -0,0 +1 @@
+start
cond
diff --git a/packages/svelte/tests/hydration/samples/cloudflare-mirage-borking-2/_override.html b/packages/svelte/tests/hydration/samples/cloudflare-mirage-borking-2/_override.html
new file mode 100644
index 000000000000..2a1c32328897
--- /dev/null
+++ b/packages/svelte/tests/hydration/samples/cloudflare-mirage-borking-2/_override.html
@@ -0,0 +1 @@
+ start
cond
diff --git a/packages/svelte/tests/hydration/samples/cloudflare-mirage-borking-2/main.svelte b/packages/svelte/tests/hydration/samples/cloudflare-mirage-borking-2/main.svelte
new file mode 100644
index 000000000000..bfb4f2cdb8cf
--- /dev/null
+++ b/packages/svelte/tests/hydration/samples/cloudflare-mirage-borking-2/main.svelte
@@ -0,0 +1,5 @@
+
+
+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 000000000000..56ba73b06408
--- /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 000000000000..f6c03b87c1c2
--- /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 000000000000..c84efbb00bd2
--- /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 000000000000..2c9a94686eb3
--- /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/samples/removes-undefined-attributes/_config.js b/packages/svelte/tests/hydration/samples/removes-undefined-attributes/_config.js
new file mode 100644
index 000000000000..bc74f23aac60
--- /dev/null
+++ b/packages/svelte/tests/hydration/samples/removes-undefined-attributes/_config.js
@@ -0,0 +1,11 @@
+import { test } from '../../test';
+
+export default test({
+ server_props: {
+ browser: false
+ },
+
+ props: {
+ browser: true
+ }
+});
diff --git a/packages/svelte/tests/hydration/samples/removes-undefined-attributes/_expected.html b/packages/svelte/tests/hydration/samples/removes-undefined-attributes/_expected.html
new file mode 100644
index 000000000000..cc789c8f5142
--- /dev/null
+++ b/packages/svelte/tests/hydration/samples/removes-undefined-attributes/_expected.html
@@ -0,0 +1 @@
+
diff --git a/packages/svelte/tests/hydration/samples/removes-undefined-attributes/main.svelte b/packages/svelte/tests/hydration/samples/removes-undefined-attributes/main.svelte
new file mode 100644
index 000000000000..1a587eeeebc0
--- /dev/null
+++ b/packages/svelte/tests/hydration/samples/removes-undefined-attributes/main.svelte
@@ -0,0 +1,9 @@
+
+
+
diff --git a/packages/svelte/tests/hydration/test.ts b/packages/svelte/tests/hydration/test.ts
index 266ac07bff39..70d5c5d0724f 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/migrate/samples/impossible-migrate-$derived-derived-var-3/output.svelte b/packages/svelte/tests/migrate/samples/impossible-migrate-$derived-derived-var-3/output.svelte
index 9e4f086aedd3..26012e11151d 100644
--- a/packages/svelte/tests/migrate/samples/impossible-migrate-$derived-derived-var-3/output.svelte
+++ b/packages/svelte/tests/migrate/samples/impossible-migrate-$derived-derived-var-3/output.svelte
@@ -1,7 +1,7 @@
-
+
-
\ No newline at end of file
+
diff --git a/packages/svelte/tests/migrate/samples/impossible-migrate-slot-change-name/output.svelte b/packages/svelte/tests/migrate/samples/impossible-migrate-slot-change-name/output.svelte
index 2b6838a1d681..328966b63b6a 100644
--- a/packages/svelte/tests/migrate/samples/impossible-migrate-slot-change-name/output.svelte
+++ b/packages/svelte/tests/migrate/samples/impossible-migrate-slot-change-name/output.svelte
@@ -1,6 +1,6 @@
-
+
-
\ No newline at end of file
+
diff --git a/packages/svelte/tests/migrate/samples/impossible-migrate-slot-non-identifier/output.svelte b/packages/svelte/tests/migrate/samples/impossible-migrate-slot-non-identifier/output.svelte
index 6e5ab103109b..1e763577df34 100644
--- a/packages/svelte/tests/migrate/samples/impossible-migrate-slot-non-identifier/output.svelte
+++ b/packages/svelte/tests/migrate/samples/impossible-migrate-slot-non-identifier/output.svelte
@@ -1,2 +1,2 @@
-
-
\ No newline at end of file
+
+
diff --git a/packages/svelte/tests/migrate/samples/jsdoc-with-comments/input.svelte b/packages/svelte/tests/migrate/samples/jsdoc-with-comments/input.svelte
index f2efb1db804c..f138c3a0707d 100644
--- a/packages/svelte/tests/migrate/samples/jsdoc-with-comments/input.svelte
+++ b/packages/svelte/tests/migrate/samples/jsdoc-with-comments/input.svelte
@@ -21,6 +21,9 @@
*/
export let type_no_comment;
+ /** @type {boolean} type_with_comment - One-line declaration with comment */
+ export let type_with_comment;
+
/**
* This is optional
*/
@@ -40,4 +43,10 @@
export let inline_multiline_trailing_comment = 'world'; /*
* this is a same-line trailing multiline comment
**/
+
+ /** @type {number} [default_value=1] */
+ export let default_value = 1;
+
+ /** @type {number} [comment_default_value=1] - This has a comment and an optional value. */
+ export let comment_default_value = 1;
\ No newline at end of file
diff --git a/packages/svelte/tests/migrate/samples/jsdoc-with-comments/output.svelte b/packages/svelte/tests/migrate/samples/jsdoc-with-comments/output.svelte
index 19fbe38b5093..32133ccd4c85 100644
--- a/packages/svelte/tests/migrate/samples/jsdoc-with-comments/output.svelte
+++ b/packages/svelte/tests/migrate/samples/jsdoc-with-comments/output.svelte
@@ -9,12 +9,18 @@
+
+
+
+
+
+
/**
* @typedef {Object} Props
* @property {string} comment - My wonderful comment
@@ -22,11 +28,14 @@
* @property {any} one_line - one line comment
* @property {any} no_comment
* @property {boolean} type_no_comment
+ * @property {boolean} type_with_comment - One-line declaration with comment
* @property {any} [optional] - This is optional
* @property {any} inline_commented - this should stay a comment
* @property {any} inline_commented_merged - This comment should be merged - with this inline comment
* @property {string} [inline_multiline_leading_comment] - this is a same-line leading multiline comment
* @property {string} [inline_multiline_trailing_comment] - this is a same-line trailing multiline comment
+ * @property {number} [default_value]
+ * @property {number} [comment_default_value] - This has a comment and an optional value.
*/
/** @type {Props} */
@@ -36,10 +45,13 @@
one_line,
no_comment,
type_no_comment,
+ type_with_comment,
optional = {stuff: true},
inline_commented,
inline_commented_merged,
inline_multiline_leading_comment = 'world',
- inline_multiline_trailing_comment = 'world'
+ inline_multiline_trailing_comment = 'world',
+ default_value = 1,
+ comment_default_value = 1
} = $props();
\ No newline at end of file
diff --git a/packages/svelte/tests/migrate/samples/labeled-statement-reassign-state/input.svelte b/packages/svelte/tests/migrate/samples/labeled-statement-reassign-state/input.svelte
new file mode 100644
index 000000000000..0b5c13d8898b
--- /dev/null
+++ b/packages/svelte/tests/migrate/samples/labeled-statement-reassign-state/input.svelte
@@ -0,0 +1,6 @@
+
\ No newline at end of file
diff --git a/packages/svelte/tests/migrate/samples/labeled-statement-reassign-state/output.svelte b/packages/svelte/tests/migrate/samples/labeled-statement-reassign-state/output.svelte
new file mode 100644
index 000000000000..c2b36a6e3028
--- /dev/null
+++ b/packages/svelte/tests/migrate/samples/labeled-statement-reassign-state/output.svelte
@@ -0,0 +1,10 @@
+
\ No newline at end of file
diff --git a/packages/svelte/tests/migrate/samples/reassigned-deriveds/input.svelte b/packages/svelte/tests/migrate/samples/reassigned-deriveds/input.svelte
new file mode 100644
index 000000000000..024f719fb96b
--- /dev/null
+++ b/packages/svelte/tests/migrate/samples/reassigned-deriveds/input.svelte
@@ -0,0 +1,10 @@
+
+
+
+
+
+{upper}
\ No newline at end of file
diff --git a/packages/svelte/tests/migrate/samples/reassigned-deriveds/output.svelte b/packages/svelte/tests/migrate/samples/reassigned-deriveds/output.svelte
new file mode 100644
index 000000000000..0903299d9599
--- /dev/null
+++ b/packages/svelte/tests/migrate/samples/reassigned-deriveds/output.svelte
@@ -0,0 +1,10 @@
+
+
+
+
+
+{upper}
\ No newline at end of file
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 3dad9bb4e523..c6af77a47b6f 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 66ce187c625a..a10d4eccf0eb 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 39a6f5f64702..e9a3e7e5da6f 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 94c60b701a44..94b60b9e5d13 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 d72bf7db1012..f241c81a933a 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 0d82cb2bb917..bf4b43b87595 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 9efe9acf8dda..3cd54b66479b 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 4d3a29180869..2e45184be928 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 672014629791..42892457059c 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 4ce069bd37c0..5256ede7bb9a 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 a439b65dd0ec..ee19d5874222 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 45b625667706..11ee562297ed 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 000000000000..4ee619728d17
--- /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 000000000000..37fb499e7b0f
--- /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 15db05904cff..42229b741fbd 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 e2bda741fa71..7829a2787fab 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 f30788d7583f..18860d615b90 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 ae52f72c5d9c..973cfb7d3374 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 000000000000..9faae8d1bf40
--- /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 000000000000..42e9880fccdd
--- /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 000000000000..4ee619728d17
--- /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 000000000000..b66ee7288f2e
--- /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/parser-modern/test.ts b/packages/svelte/tests/parser-modern/test.ts
index b47d4a48796e..279ba7bc08ed 100644
--- a/packages/svelte/tests/parser-modern/test.ts
+++ b/packages/svelte/tests/parser-modern/test.ts
@@ -21,6 +21,8 @@ const { test, run } = suite(async (config, cwd) => {
)
);
+ delete actual.comments;
+
// run `UPDATE_SNAPSHOTS=true pnpm test parser` to update parser tests
if (process.env.UPDATE_SNAPSHOTS) {
fs.writeFileSync(`${cwd}/output.json`, JSON.stringify(actual, null, '\t'));
diff --git a/packages/svelte/tests/runtime-browser/assert.js b/packages/svelte/tests/runtime-browser/assert.js
index fb460c722a0f..48bde014105c 100644
--- a/packages/svelte/tests/runtime-browser/assert.js
+++ b/packages/svelte/tests/runtime-browser/assert.js
@@ -1,5 +1,8 @@
/** @import { assert } from 'vitest' */
/** @import { CompileOptions, Warning } from '#compiler' */
+
+import { ELEMENT_NODE } from '#client/constants';
+
/**
* @param {any} a
* @param {any} b
@@ -102,7 +105,7 @@ function normalize_children(node) {
}
for (let child of [...node.childNodes]) {
- if (child.nodeType === 1 /* Element */) {
+ if (child.nodeType === ELEMENT_NODE) {
normalize_children(child);
}
}
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 000000000000..6502a08290c5
--- /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 000000000000..ddd2d4b61ac6
--- /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 000000000000..694b26f231c6
--- /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 000000000000..06f28c4f75df
--- /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 000000000000..762963383532
--- /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 000000000000..055ce57da51a
--- /dev/null
+++ b/packages/svelte/tests/runtime-browser/samples/css-props-dynamic-component/main.svelte
@@ -0,0 +1,11 @@
+
+
+value++}>click
+
+
\ 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 582a10edf722..63e601b115fa 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 000000000000..5d3725235842
--- /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 000000000000..fbec108c7a01
--- /dev/null
+++ b/packages/svelte/tests/runtime-legacy/samples/attachment-in-mutated-state/main.svelte
@@ -0,0 +1,15 @@
+
+
+ state.count++}>{state.count}
+
+{#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 29ddac16ad89..9d47f86aad88 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: `
- wheeee
+ wheeee
`
});
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 d0b46cffa6d5..6e1feb70fac2 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: `
- put your left leg in
+ put your left leg in
your left leg out
in, out, in, out
shake it all about
@@ -36,7 +36,28 @@ export default test({
shake it all about
`,
- async test({ assert, component, target, window }) {
+ async test({ assert, component, target, window, variant }) {
+ assert.htmlEqual(
+ target.innerHTML,
+ `
+
+ put your left leg in
+ your left leg out
+ in, out, in, out
+ shake it all about
+
+
+
+ put your left leg in
+
+
+ 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,
`
- put your left leg in
+ put your left leg in
your left leg out
in, out, in, out
shake it all about
@@ -94,7 +115,7 @@ export default test({
target.innerHTML,
`
- put your left leg in
+ put your left leg in
your left leg out
in, out, in, out
shake it all about
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 000000000000..d20e4a1e88d3
--- /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: `
+
+
+
+
+ remaining:one / done:two / remaining:three
+ `,
+
+ ssrHtml: `
+
+
+
+
+ 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,
+ `
+
+
+
+ remaining:one / done:two / remaining:three
+ `
+ );
+
+ set_text(1, 'four');
+ flushSync();
+
+ assert.htmlEqual(
+ target.innerHTML,
+ `
+
+
+
+ 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,
+ `
+
+
+ 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,
+ `
+
+
+
+ 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 000000000000..7e1019953036
--- /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}
+
+{/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 d20e4a1e88d3..c8019e11e94a 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 7e1019953036..9ca7832a72c9 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 @@
+
+{#if a = 0}{/if}
+
+{#each [b = 0] as x}{x,''}{/each}
+
+{#key c = 0}{/key}
+
+{#await d = 0}{/await}
+
+{#snippet snip()}{/snippet}
+
+{@render (e = 0, snip)()}
+
+{@html f = 0, ''}
+
+
+
+{#key 1}
+ {@const x = (h = 0)}
+ {x, ''}
+{/key}
+
+{#if 1}
+ {@const x = (i = 0)}
+ {x, ''}
+{/if}
+
+
inc
+[{a},{b},{c},{d},{e},{f},{g},{h},{i}]
+
+
diff --git a/packages/svelte/tests/runtime-legacy/samples/block-expression-fn-call/_config.js b/packages/svelte/tests/runtime-legacy/samples/block-expression-fn-call/_config.js
new file mode 100644
index 000000000000..523dcd625dce
--- /dev/null
+++ b/packages/svelte/tests/runtime-legacy/samples/block-expression-fn-call/_config.js
@@ -0,0 +1,12 @@
+import { flushSync } from 'svelte';
+import { test } from '../../test';
+
+export default test({
+ test({ assert, target }) {
+ const button = target.querySelector('button');
+
+ assert.htmlEqual(target.innerHTML, `
inc 12 - 12`);
+ flushSync(() => button?.click());
+ assert.htmlEqual(target.innerHTML, `
inc 13 - 12`);
+ }
+});
diff --git a/packages/svelte/tests/runtime-legacy/samples/block-expression-fn-call/main.svelte b/packages/svelte/tests/runtime-legacy/samples/block-expression-fn-call/main.svelte
new file mode 100644
index 000000000000..37838f091fdf
--- /dev/null
+++ b/packages/svelte/tests/runtime-legacy/samples/block-expression-fn-call/main.svelte
@@ -0,0 +1,36 @@
+
+
+{#if fn(false)}{:else if fn(true)}{/if}
+
+{#each fn([]) as x}{x, ''}{/each}
+
+{#key fn(1)}{/key}
+
+{#await fn(Promise.resolve())}{/await}
+
+{#snippet snip()}{/snippet}
+
+{@render fn(snip)()}
+
+{@html fn('')}
+
+
{})}>
+
+{#key 1}
+ {@const x = fn('')}
+ {x}
+{/key}
+
+
count1++}>{fn('inc')}
+{count1} - {count2}
+
+
diff --git a/packages/svelte/tests/runtime-legacy/samples/block-expression-member-access/_config.js b/packages/svelte/tests/runtime-legacy/samples/block-expression-member-access/_config.js
new file mode 100644
index 000000000000..0e1a5a81502f
--- /dev/null
+++ b/packages/svelte/tests/runtime-legacy/samples/block-expression-member-access/_config.js
@@ -0,0 +1,12 @@
+import { flushSync } from 'svelte';
+import { test } from '../../test';
+
+export default test({
+ test({ assert, target }) {
+ const button = target.querySelector('button');
+
+ assert.htmlEqual(target.innerHTML, `
inc 10 - 10`);
+ flushSync(() => button?.click());
+ assert.htmlEqual(target.innerHTML, `
inc 11 - 10`);
+ }
+});
diff --git a/packages/svelte/tests/runtime-legacy/samples/block-expression-member-access/main.svelte b/packages/svelte/tests/runtime-legacy/samples/block-expression-member-access/main.svelte
new file mode 100644
index 000000000000..4041be4f6fda
--- /dev/null
+++ b/packages/svelte/tests/runtime-legacy/samples/block-expression-member-access/main.svelte
@@ -0,0 +1,46 @@
+
+
+{#if obj.false}{:else if obj.true}{/if}
+
+{#each obj.array as x}{x, ''}{/each}
+
+{#key obj.string}{/key}
+
+{#await obj.promise}{/await}
+
+{#snippet snip()}{/snippet}
+
+{@render obj.snippet()}
+
+{@html obj.string}
+
+
+
+{#key 1}
+ {@const x = obj.string}
+ {x}
+{/key}
+
+
count1++}>inc
+{count1} - {count2}
+
+
diff --git a/packages/svelte/tests/runtime-legacy/samples/inline-style-directive-spread-and-attr-empty/_config.js b/packages/svelte/tests/runtime-legacy/samples/inline-style-directive-spread-and-attr-empty/_config.js
index 9ff0007c3713..04c9868ac378 100644
--- a/packages/svelte/tests/runtime-legacy/samples/inline-style-directive-spread-and-attr-empty/_config.js
+++ b/packages/svelte/tests/runtime-legacy/samples/inline-style-directive-spread-and-attr-empty/_config.js
@@ -2,6 +2,6 @@ import { test } from '../../test';
export default test({
html: `
-
+
`
});
diff --git a/packages/svelte/tests/runtime-legacy/samples/inline-style-optimisation-bailout/_config.js b/packages/svelte/tests/runtime-legacy/samples/inline-style-optimisation-bailout/_config.js
index adcdc4706d88..e9965b2b1e26 100644
--- a/packages/svelte/tests/runtime-legacy/samples/inline-style-optimisation-bailout/_config.js
+++ b/packages/svelte/tests/runtime-legacy/samples/inline-style-optimisation-bailout/_config.js
@@ -2,7 +2,7 @@ import { ok, test } from '../../test';
export default test({
html: `
-
color: red
+
color: red;
`,
test({ assert, component, target, window }) {
diff --git a/packages/svelte/tests/runtime-legacy/samples/inline-style-optimisation-bailout/main.svelte b/packages/svelte/tests/runtime-legacy/samples/inline-style-optimisation-bailout/main.svelte
index 35b768547e25..e07adaa1c9d8 100644
--- a/packages/svelte/tests/runtime-legacy/samples/inline-style-optimisation-bailout/main.svelte
+++ b/packages/svelte/tests/runtime-legacy/samples/inline-style-optimisation-bailout/main.svelte
@@ -1,5 +1,5 @@
{styles}
\ No newline at end of file
diff --git a/packages/svelte/tests/runtime-legacy/samples/input-list/_config.js b/packages/svelte/tests/runtime-legacy/samples/input-list/_config.js
index fe6a29207d4c..1e95aaafa6d9 100644
--- a/packages/svelte/tests/runtime-legacy/samples/input-list/_config.js
+++ b/packages/svelte/tests/runtime-legacy/samples/input-list/_config.js
@@ -4,7 +4,9 @@ export default test({
html: `
-
+
+
+
`
});
diff --git a/packages/svelte/tests/runtime-legacy/samples/lifecycle-render-order-for-children/Item.svelte b/packages/svelte/tests/runtime-legacy/samples/lifecycle-render-order-for-children/Item.svelte
index b2e6cd046c8e..4127e857d5d5 100644
--- a/packages/svelte/tests/runtime-legacy/samples/lifecycle-render-order-for-children/Item.svelte
+++ b/packages/svelte/tests/runtime-legacy/samples/lifecycle-render-order-for-children/Item.svelte
@@ -5,7 +5,7 @@
export let index;
export let n;
- function logRender () {
+ function logRender (n) {
order.push(`${index}: render ${n}`);
return index;
}
@@ -24,5 +24,5 @@
- {logRender()}
+ {logRender(n)}
diff --git a/packages/svelte/tests/runtime-legacy/samples/lifecycle-render-order-for-children/main.svelte b/packages/svelte/tests/runtime-legacy/samples/lifecycle-render-order-for-children/main.svelte
index b05b1476fdcc..51dee3bc0c25 100644
--- a/packages/svelte/tests/runtime-legacy/samples/lifecycle-render-order-for-children/main.svelte
+++ b/packages/svelte/tests/runtime-legacy/samples/lifecycle-render-order-for-children/main.svelte
@@ -5,7 +5,7 @@
export let n = 0;
- function logRender () {
+ function logRender (n) {
order.push(`parent: render ${n}`);
return 'parent';
}
@@ -23,7 +23,7 @@
})
-{logRender()}
+{logRender(n)}
{#each [1,2,3] as index}
diff --git a/packages/svelte/tests/runtime-legacy/samples/namespace-html/_config.js b/packages/svelte/tests/runtime-legacy/samples/namespace-html/_config.js
index 3be9f0e92539..b7ecd04def65 100644
--- a/packages/svelte/tests/runtime-legacy/samples/namespace-html/_config.js
+++ b/packages/svelte/tests/runtime-legacy/samples/namespace-html/_config.js
@@ -9,7 +9,7 @@ export default test({
-
+
hi
`,
diff --git a/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access-2/Component.svelte b/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access-2/Component.svelte
new file mode 100644
index 000000000000..73347c4d7ff1
--- /dev/null
+++ b/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access-2/Component.svelte
@@ -0,0 +1,11 @@
+
+
+{my_prop.foo}
diff --git a/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access-2/_config.js b/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access-2/_config.js
new file mode 100644
index 000000000000..81005cf73760
--- /dev/null
+++ b/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access-2/_config.js
@@ -0,0 +1,14 @@
+import { test } from '../../test';
+import { flushSync } from 'svelte';
+
+export default test({
+ async test({ assert, target, logs }) {
+ const [btn1] = target.querySelectorAll('button');
+
+ flushSync(() => {
+ btn1.click();
+ });
+
+ assert.deepEqual(logs, ['bar']);
+ }
+});
diff --git a/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access-2/main.svelte b/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access-2/main.svelte
new file mode 100644
index 000000000000..f38b37fb7f7c
--- /dev/null
+++ b/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access-2/main.svelte
@@ -0,0 +1,15 @@
+
+
+ {
+ value = undefined;
+ }}>Reset value
+
+{#if value !== undefined}
+
+{/if}
diff --git a/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access-3/Component.svelte b/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access-3/Component.svelte
new file mode 100644
index 000000000000..5bfb7771289d
--- /dev/null
+++ b/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access-3/Component.svelte
@@ -0,0 +1,5 @@
+
+
+
diff --git a/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access-3/_config.js b/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access-3/_config.js
new file mode 100644
index 000000000000..0eb68310cbb6
--- /dev/null
+++ b/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access-3/_config.js
@@ -0,0 +1,11 @@
+import { test } from '../../test';
+import { flushSync } from 'svelte';
+
+export default test({
+ async test({ target }) {
+ const [btn1] = target.querySelectorAll('button');
+
+ btn1.click();
+ flushSync();
+ }
+});
diff --git a/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access-3/main.svelte b/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access-3/main.svelte
new file mode 100644
index 000000000000..9c72d2c48ac1
--- /dev/null
+++ b/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access-3/main.svelte
@@ -0,0 +1,16 @@
+
+
+{#if state}
+ {@const attributes = { title: state.title }}
+
+{/if}
+ {
+ state = undefined;
+ }}
+>
+ Del
+
diff --git a/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access/Component.svelte b/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access/Component.svelte
new file mode 100644
index 000000000000..761f303c2e0c
--- /dev/null
+++ b/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access/Component.svelte
@@ -0,0 +1,12 @@
+
+
+{count}
+
+ count-- }>
diff --git a/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access/_config.js b/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access/_config.js
new file mode 100644
index 000000000000..4f75e82aaea7
--- /dev/null
+++ b/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access/_config.js
@@ -0,0 +1,66 @@
+import { test } from '../../test';
+import { flushSync } from 'svelte';
+
+export default test({
+ async test({ assert, target, logs }) {
+ const [btn1, btn2, btn3] = target.querySelectorAll('button');
+ let ps = [...target.querySelectorAll('p')];
+
+ for (const p of ps) {
+ assert.equal(p.innerHTML, '0');
+ }
+
+ flushSync(() => {
+ btn1.click();
+ });
+
+ // prop update normally if we are not unmounting
+ for (const p of ps) {
+ assert.equal(p.innerHTML, '1');
+ }
+
+ flushSync(() => {
+ btn3.click();
+ });
+
+ // binding still works and update the value correctly
+ for (const p of ps) {
+ assert.equal(p.innerHTML, '0');
+ }
+
+ flushSync(() => {
+ btn1.click();
+ });
+
+ flushSync(() => {
+ btn1.click();
+ });
+
+ // the five components guarded by `count < 2` unmount and log
+ assert.deepEqual(logs, [1, true, 1, true, 1, true, 1, true, 1, true]);
+
+ flushSync(() => {
+ btn2.click();
+ });
+
+ // the three components guarded by `show` unmount and log
+ assert.deepEqual(logs, [
+ 1,
+ true,
+ 1,
+ true,
+ 1,
+ true,
+ 1,
+ true,
+ 1,
+ true,
+ 2,
+ true,
+ 2,
+ true,
+ 2,
+ true
+ ]);
+ }
+});
diff --git a/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access/main.svelte b/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access/main.svelte
new file mode 100644
index 000000000000..73a7501e9db2
--- /dev/null
+++ b/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access/main.svelte
@@ -0,0 +1,41 @@
+
+
+ count++ }>
+ show = !show }>
+
+
+{#if count < 2}
+
+{/if}
+
+
+{#if count < 2}
+
+{/if}
+
+
+{#if count < 2}
+
+{/if}
+
+
+{#if show}
+
+{/if}
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/svelte/tests/runtime-legacy/samples/prop-no-change/_config.js b/packages/svelte/tests/runtime-legacy/samples/prop-no-change/_config.js
index 905c2a6226b2..84658336e246 100644
--- a/packages/svelte/tests/runtime-legacy/samples/prop-no-change/_config.js
+++ b/packages/svelte/tests/runtime-legacy/samples/prop-no-change/_config.js
@@ -2,6 +2,7 @@ import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
+ accessors: false,
test({ assert, logs, target }) {
assert.deepEqual(logs, ['primitive', 'object']);
target.querySelector('button')?.click();
diff --git a/packages/svelte/tests/runtime-legacy/samples/select-in-each/_config.js b/packages/svelte/tests/runtime-legacy/samples/select-in-each/_config.js
index 335c46d53db4..df03b7a053bc 100644
--- a/packages/svelte/tests/runtime-legacy/samples/select-in-each/_config.js
+++ b/packages/svelte/tests/runtime-legacy/samples/select-in-each/_config.js
@@ -2,27 +2,28 @@ import { flushSync } from 'svelte';
import { ok, test } from '../../test';
export default test({
- html: `
-
- A
- B
-
- selected: a
- `,
-
- test({ assert, target }) {
+ test({ assert, target, variant }) {
+ assert.htmlEqual(
+ target.innerHTML,
+ `
+
+ A
+ B
+
+ selected: a
+ `
+ );
const select = target.querySelector('select');
ok(select);
const event = new window.Event('change');
select.value = 'b';
select.dispatchEvent(event);
flushSync();
-
assert.htmlEqual(
target.innerHTML,
`
- A
+ A
B
selected: b
diff --git a/packages/svelte/tests/runtime-legacy/samples/template/_config.js b/packages/svelte/tests/runtime-legacy/samples/template/_config.js
index f82716854293..7576b6fbb8c2 100644
--- a/packages/svelte/tests/runtime-legacy/samples/template/_config.js
+++ b/packages/svelte/tests/runtime-legacy/samples/template/_config.js
@@ -1,3 +1,4 @@
+import { COMMENT_NODE } from '#client/constants';
import { ok, test } from '../../test';
export default test({
@@ -41,7 +42,7 @@ export default test({
// get all childNodes of template3 except comments
let childNodes = [];
for (const node of template3.content.childNodes) {
- if (node.nodeType !== 8) {
+ if (node.nodeType !== COMMENT_NODE) {
childNodes.push(/** @type {Element} */ (node));
}
}
diff --git a/packages/svelte/tests/runtime-legacy/shared.ts b/packages/svelte/tests/runtime-legacy/shared.ts
index fc748ce6b299..126992a0c070 100644
--- a/packages/svelte/tests/runtime-legacy/shared.ts
+++ b/packages/svelte/tests/runtime-legacy/shared.ts
@@ -3,11 +3,11 @@ import { setImmediate } from 'node:timers/promises';
import { globSync } from 'tinyglobby';
import { createClassComponent } from 'svelte/legacy';
import { proxy } from 'svelte/internal/client';
-import { flushSync, hydrate, mount, unmount } from 'svelte';
+import { flushSync, hydrate, mount, unmount, untrack } from 'svelte';
import { render } from 'svelte/server';
import { afterAll, assert, beforeAll } from 'vitest';
-import { compile_directory } from '../helpers.js';
-import { setup_html_equal } from '../html_equal.js';
+import { compile_directory, fragments } from '../helpers.js';
+import { assert_html_equal, assert_html_equal_with_options } from '../html_equal.js';
import { raf } from '../animation-helpers.js';
import type { CompileOptions } from '#compiler';
import { suite_with_variants, type BaseTest } from '../suite.js';
@@ -86,10 +86,6 @@ function unhandled_rejection_handler(err: Error) {
const listeners = process.rawListeners('unhandledRejection');
-const { assert_html_equal, assert_html_equal_with_options } = setup_html_equal({
- removeDataSvelte: true
-});
-
beforeAll(() => {
// @ts-expect-error TODO huh?
process.prependListener('unhandledRejection', unhandled_rejection_handler);
@@ -158,10 +154,14 @@ async function common_setup(cwd: string, runes: boolean | undefined, config: Run
rootDir: cwd,
dev: force_hmr ? true : undefined,
hmr: force_hmr ? true : undefined,
+ fragments,
...config.compileOptions,
immutable: config.immutable,
accessors: 'accessors' in config ? config.accessors : true,
- runes
+ runes:
+ config.compileOptions && 'runes' in config.compileOptions
+ ? config.compileOptions.runes
+ : runes
};
// load_compiled can be used for debugging a test. It means the compiler will not run on the input
@@ -351,7 +351,7 @@ async function run_test_variant(
// @ts-expect-error
globalThis.__svelte.uid = 1;
- if (manual_hydrate) {
+ if (manual_hydrate && variant === 'hydrate') {
hydrate_fn = () => {
instance = hydrate(mod.default, {
target,
@@ -469,10 +469,6 @@ async function run_test_variant(
throw err;
}
} finally {
- console.log = console_log;
- console.warn = console_warn;
- console.error = console_error;
-
config.after_test?.();
// Free up the microtask queue
@@ -486,6 +482,10 @@ async function run_test_variant(
process.on('unhandledRejection', listener);
});
}
+
+ console.log = console_log;
+ console.warn = console_warn;
+ console.error = console_error;
}
}
diff --git a/packages/svelte/tests/runtime-runes/samples/array-sort-in-effect/_config.js b/packages/svelte/tests/runtime-runes/samples/array-sort-in-effect/_config.js
new file mode 100644
index 000000000000..cbac36fee8ef
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/array-sort-in-effect/_config.js
@@ -0,0 +1,52 @@
+import { flushSync } from 'svelte';
+import { test } from '../../test';
+
+export default test({
+ /**
+ * Ensure that sorting an array inside an $effect works correctly
+ * and re-runs when the array changes (e.g., when items are added).
+ */
+ test({ assert, target }) {
+ const button = target.querySelector('button');
+
+ // initial render — array should be sorted
+ assert.htmlEqual(
+ target.innerHTML,
+ `
+ add item
+ 0
+ 50
+ 100
+ `
+ );
+
+ // add first item (20); effect should re-run and sort the array
+ flushSync(() => button?.click());
+
+ assert.htmlEqual(
+ target.innerHTML,
+ `
+ add item
+ 0
+ 20
+ 50
+ 100
+ `
+ );
+
+ // add second item (80); effect should re-run and sort the array
+ flushSync(() => button?.click());
+
+ assert.htmlEqual(
+ target.innerHTML,
+ `
+ add item
+ 0
+ 20
+ 50
+ 80
+ 100
+ `
+ );
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/array-sort-in-effect/main.svelte b/packages/svelte/tests/runtime-runes/samples/array-sort-in-effect/main.svelte
new file mode 100644
index 000000000000..c529f67cf4e5
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/array-sort-in-effect/main.svelte
@@ -0,0 +1,21 @@
+
+
+add item
+{#each arr as x}
+ {x}
+{/each}
diff --git a/packages/svelte/tests/runtime-runes/samples/attachment-basic/_config.js b/packages/svelte/tests/runtime-runes/samples/attachment-basic/_config.js
new file mode 100644
index 000000000000..1be47370691a
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/attachment-basic/_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-basic/main.svelte b/packages/svelte/tests/runtime-runes/samples/attachment-basic/main.svelte
new file mode 100644
index 000000000000..1a1f74e4a94a
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/attachment-basic/main.svelte
@@ -0,0 +1 @@
+ node.textContent = node.nodeName}>
diff --git a/packages/svelte/tests/runtime-runes/samples/attachment-component-falsy/Child.svelte b/packages/svelte/tests/runtime-runes/samples/attachment-component-falsy/Child.svelte
new file mode 100644
index 000000000000..6760da61faeb
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/attachment-component-falsy/Child.svelte
@@ -0,0 +1,5 @@
+
+
+
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 000000000000..27f013d6d179
--- /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 000000000000..993bcdd6a561
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/attachment-component-falsy/main.svelte
@@ -0,0 +1,13 @@
+
+
+ enabled = !enabled}>
+
+
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 000000000000..6760da61faeb
--- /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 000000000000..23907c62d2ba
--- /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: `update
`,
+
+ 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 000000000000..d1d7e6512638
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/attachment-component-spread/main.svelte
@@ -0,0 +1,30 @@
+
+
+update
+
+
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 000000000000..6760da61faeb
--- /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 000000000000..b6ef016be5d4
--- /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: `update
`,
+ html: `update one
`,
+
+ test({ target, assert }) {
+ const button = target.querySelector('button');
+
+ flushSync(() => button?.click());
+ assert.htmlEqual(target.innerHTML, 'update 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 000000000000..29e26689dba6
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/attachment-component/main.svelte
@@ -0,0 +1,15 @@
+
+
+ message = 'two'}>update
+
+
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 000000000000..2e53f0d29dd4
--- /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 000000000000..35079aa15e5c
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/attachment-from-action/main.svelte
@@ -0,0 +1,37 @@
+
+
+{#if count < 2}
+
+ count)}>
+{/if}
+
+ count++}>
\ 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 000000000000..7d0502590b5d
--- /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: `
increment `,
+ html: `1
increment `,
+
+ test: ({ assert, target }) => {
+ const btn = target.querySelector('button');
+
+ flushSync(() => btn?.click());
+ assert.htmlEqual(target.innerHTML, `2
increment `);
+ }
+});
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 000000000000..9fa3cfdb6798
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/attachment-reactive/main.svelte
@@ -0,0 +1,6 @@
+
+
+ node.textContent = value}>
+ value += 1}>increment
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 000000000000..d5f4e943583a
--- /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 000000000000..5d3725235842
--- /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 000000000000..aef9da6dd46f
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/attachment-spread-stable/main.svelte
@@ -0,0 +1,17 @@
+
+
+ count++}>{count}
+
+{#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 000000000000..96fc20745025
--- /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 000000000000..dbd8c47ada1e
--- /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 000000000000..1be47370691a
--- /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 000000000000..bd4b52342f32
--- /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/await-hydrate-maybe-promise/_config.js b/packages/svelte/tests/runtime-runes/samples/await-hydrate-maybe-promise/_config.js
new file mode 100644
index 000000000000..f81b41d41ae9
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/await-hydrate-maybe-promise/_config.js
@@ -0,0 +1,23 @@
+import { flushSync } from 'svelte';
+import { test } from '../../test';
+
+export default test({
+ ssrHtml: 'fulfil 42
loading...
',
+ html: 'fulfil loading...
42
',
+
+ props: {
+ browser: true
+ },
+
+ server_props: {
+ browser: false
+ },
+
+ async test({ assert, target }) {
+ const button = target.querySelector('button');
+
+ flushSync(() => button?.click());
+ await Promise.resolve();
+ assert.htmlEqual(target.innerHTML, 'fulfil 42
42
');
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/await-hydrate-maybe-promise/main.svelte b/packages/svelte/tests/runtime-runes/samples/await-hydrate-maybe-promise/main.svelte
new file mode 100644
index 000000000000..d8d0cd402750
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/await-hydrate-maybe-promise/main.svelte
@@ -0,0 +1,25 @@
+
+
+ fulfil(42)}>fulfil
+
+{#await a}
+ {#if true}loading...
{/if}
+{:then a}
+ {a}
+{/await}
+
+
+
+{#await b}
+ {#if true}loading...
{/if}
+{:then b}
+ {b}
+{/await}
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 000000000000..810ac338a5cc
--- /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 000000000000..126528bad204
--- /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 ff13af85c896..0886efd59c27 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 000000000000..0fdeabfe0b8e
--- /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,
+ `
+ inc
+ 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 000000000000..746f22b1e618
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/class-private-fields-assignment-shorthand/main.svelte
@@ -0,0 +1,28 @@
+
+
+ counter.inc()}>inc
+
+{#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 000000000000..dd847ce2f2a6
--- /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: `10 `,
+ ssrHtml: `0 `,
+
+ async test({ assert, target }) {
+ flushSync();
+
+ assert.htmlEqual(target.innerHTML, `10 `);
+ }
+});
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 000000000000..3d8ea414187e
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/class-state-constructor-closure-private-2/main.svelte
@@ -0,0 +1,13 @@
+
+
+ counter.count++}>{counter.count}
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 000000000000..dd847ce2f2a6
--- /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: `10 `,
+ ssrHtml: `0 `,
+
+ async test({ assert, target }) {
+ flushSync();
+
+ assert.htmlEqual(target.innerHTML, `10 `);
+ }
+});
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 000000000000..47b8c901eb95
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/class-state-constructor-closure-private-3/main.svelte
@@ -0,0 +1,12 @@
+
+
+ counter.count++}>{counter.count}
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 000000000000..f47bee71df87
--- /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 000000000000..e2c4f302b397
--- /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 000000000000..4cf1aea213dc
--- /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 000000000000..03687d01bb3d
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/class-state-constructor-derived-unowned/main.svelte
@@ -0,0 +1,37 @@
+
+
+
+
+
+ clicks: {someLogic.someValue}
+
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 000000000000..02cf36d900cc
--- /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: `0 `,
+
+ test({ assert, target }) {
+ const btn = target.querySelector('button');
+
+ flushSync(() => {
+ btn?.click();
+ });
+ assert.htmlEqual(target.innerHTML, `1 `);
+
+ flushSync(() => {
+ btn?.click();
+ });
+ assert.htmlEqual(target.innerHTML, `2 `);
+ }
+});
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 000000000000..5dbbb10afd35
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/class-state-constructor-predeclared-field/main.svelte
@@ -0,0 +1,12 @@
+
+
+ counter.count++}>{counter.count}
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 000000000000..32cca6c69375
--- /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: `10: 20 `,
+
+ test({ assert, target }) {
+ const btn = target.querySelector('button');
+
+ flushSync(() => {
+ btn?.click();
+ });
+ assert.htmlEqual(target.innerHTML, `11: 22 `);
+
+ flushSync(() => {
+ btn?.click();
+ });
+ assert.htmlEqual(target.innerHTML, `12: 24 `);
+ }
+});
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 000000000000..d8feb554cd18
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/class-state-constructor-subclass/main.svelte
@@ -0,0 +1,22 @@
+
+
+{counter.count}: {counter.custom}
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 000000000000..f35dc57228a1
--- /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: `20 `,
+
+ test({ assert, target }) {
+ const btn = target.querySelector('button');
+
+ flushSync(() => {
+ btn?.click();
+ });
+ assert.htmlEqual(target.innerHTML, `22 `);
+
+ flushSync(() => {
+ btn?.click();
+ });
+ assert.htmlEqual(target.innerHTML, `24 `);
+ }
+});
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 000000000000..aa8ba1658b03
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/class-state-constructor/main.svelte
@@ -0,0 +1,18 @@
+
+
+{counter.doubled}
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 141d994a2f85..40ef84a2e6f5 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: `
0
doubled: 0
+ tripled: 0
`,
test({ assert, target }) {
@@ -17,6 +18,7 @@ export default test({
`
1
doubled: 2
+ tripled: 3
`
);
@@ -27,6 +29,7 @@ export default test({
`
2
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 2c4c8f183970..d971566396d6 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);
counter.count++}>{counter.count}
-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 4c98245e5b6b..82774f160d3b 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/delete-proxy-key/_config.js b/packages/svelte/tests/runtime-runes/samples/delete-proxy-key/_config.js
new file mode 100644
index 000000000000..23141a01cd86
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/delete-proxy-key/_config.js
@@ -0,0 +1,13 @@
+import { test } from '../../test';
+import { flushSync } from 'svelte';
+
+export default test({
+ html: `delete test
`,
+
+ async test({ assert, target }) {
+ const btn = target.querySelector('button');
+
+ flushSync(() => btn?.click());
+ assert.htmlEqual(target.innerHTML, 'delete ');
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/delete-proxy-key/main.svelte b/packages/svelte/tests/runtime-runes/samples/delete-proxy-key/main.svelte
new file mode 100644
index 000000000000..d3c519c401c8
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/delete-proxy-key/main.svelte
@@ -0,0 +1,13 @@
+
+
+ delete obj.test}>
+ delete
+
+
+{#each keys as key}
+ {key}
+{/each}
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 000000000000..7f8d1e000d6b
--- /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: `increment a: 1
b: 2
c: 3
`,
+
+ test({ assert, target }) {
+ const button = target.querySelector('button');
+
+ flushSync(() => button?.click());
+ assert.htmlEqual(
+ target.innerHTML,
+ `increment 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 000000000000..8c8629a72c75
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/derived-destructured-iterator/main.svelte
@@ -0,0 +1,16 @@
+
+
+ offset += 1}>increment
+
+a: {a}
+b: {b}
+c: {c}
diff --git a/packages/svelte/tests/runtime-runes/samples/deriveds-in-constructor/_config.js b/packages/svelte/tests/runtime-runes/samples/deriveds-in-constructor/_config.js
new file mode 100644
index 000000000000..b364a989f480
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/deriveds-in-constructor/_config.js
@@ -0,0 +1,5 @@
+import { test } from '../../test';
+
+export default test({
+ html: `state,derived state,derived.by derived state
`
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/deriveds-in-constructor/main.svelte b/packages/svelte/tests/runtime-runes/samples/deriveds-in-constructor/main.svelte
new file mode 100644
index 000000000000..bc8efba7e7c2
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/deriveds-in-constructor/main.svelte
@@ -0,0 +1,18 @@
+
+
+{foo.initial}
\ No newline at end of file
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 000000000000..0996a02b6065
--- /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 000000000000..345654535b42
--- /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 000000000000..518f73314474
--- /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/dynamic-component-nested/A.svelte b/packages/svelte/tests/runtime-runes/samples/dynamic-component-nested/A.svelte
new file mode 100644
index 000000000000..d37c929273be
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/dynamic-component-nested/A.svelte
@@ -0,0 +1,5 @@
+
+
+{@render children()}
diff --git a/packages/svelte/tests/runtime-runes/samples/dynamic-component-nested/_config.js b/packages/svelte/tests/runtime-runes/samples/dynamic-component-nested/_config.js
new file mode 100644
index 000000000000..cd1fa2b1b9a1
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/dynamic-component-nested/_config.js
@@ -0,0 +1,8 @@
+import { test } from '../../test';
+import { flushSync } from 'svelte';
+
+export default test({
+ async test({ assert, target }) {
+ assert.htmlEqual(target.innerHTML, 'test');
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/dynamic-component-nested/main.svelte b/packages/svelte/tests/runtime-runes/samples/dynamic-component-nested/main.svelte
new file mode 100644
index 000000000000..d0646b319b40
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/dynamic-component-nested/main.svelte
@@ -0,0 +1,9 @@
+
+
+
+ test
+
diff --git a/packages/svelte/tests/runtime-runes/samples/dynamic-style-attr/_config.js b/packages/svelte/tests/runtime-runes/samples/dynamic-style-attr/_config.js
index f6829721795c..20092ddadf34 100644
--- a/packages/svelte/tests/runtime-runes/samples/dynamic-style-attr/_config.js
+++ b/packages/svelte/tests/runtime-runes/samples/dynamic-style-attr/_config.js
@@ -2,7 +2,7 @@ import { test } from '../../test';
import { flushSync } from 'svelte';
export default test({
- html: `Hello world
Make blue Hello world Make blue Hello worldMake blue Hello worldMake blue color;
-Hello world
+Hello world
color = 'blue'}>Make blue
diff --git a/packages/svelte/tests/runtime-runes/samples/effect-cleanup/_config.js b/packages/svelte/tests/runtime-runes/samples/effect-cleanup/_config.js
index 6a3d9eef7702..e55733c14810 100644
--- a/packages/svelte/tests/runtime-runes/samples/effect-cleanup/_config.js
+++ b/packages/svelte/tests/runtime-runes/samples/effect-cleanup/_config.js
@@ -10,6 +10,6 @@ export default test({
flushSync(() => {
b1.click();
});
- assert.deepEqual(logs, ['init 0', 'cleanup 2', null, 'init 2', 'cleanup 4', null, 'init 4']);
+ assert.deepEqual(logs, ['init 0']);
}
});
diff --git a/packages/svelte/tests/runtime-runes/samples/effect-root-5/_config.js b/packages/svelte/tests/runtime-runes/samples/effect-root-5/_config.js
new file mode 100644
index 000000000000..260c757e3d8e
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/effect-root-5/_config.js
@@ -0,0 +1,17 @@
+import { flushSync } from 'svelte';
+import { test } from '../../test';
+
+export default test({
+ async test({ assert, target, logs }) {
+ const [b1, b2] = target.querySelectorAll('button');
+
+ flushSync(() => b1.click());
+ assert.deepEqual(logs, [0, 1]);
+
+ flushSync(() => b1.click());
+ assert.deepEqual(logs, [0, 1, 2]);
+
+ flushSync(() => b2.click());
+ assert.deepEqual(logs, [0, 1, 2]);
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/effect-root-5/main.svelte b/packages/svelte/tests/runtime-runes/samples/effect-root-5/main.svelte
new file mode 100644
index 000000000000..06655a53623c
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/effect-root-5/main.svelte
@@ -0,0 +1,23 @@
+
+
+ ((obj ??= { count: 0 }).count += 1)}>+1
+ (obj = null)}>null
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 000000000000..1016ffb43e7e
--- /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: `toggle (false) `,
+
+ 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 000000000000..fff81591d5c6
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/effect-teardown-stale-value/main.svelte
@@ -0,0 +1,14 @@
+
+
+ foo = !foo}>toggle ({foo})
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 b3edfefa2002..4e771dfafa04 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, `change 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 bae46c2590dc..d9dee1e2b037 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 @@
count++}>change
- {#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 6e607871d38f..de482da985e9 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 b3edfefa2002..3825dc781194 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, `change Error occured
`);
+ assert.htmlEqual(target.innerHTML, `change 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 65b71106fab9..56e484667597 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]
- })
count++}>change
-
+
{#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 fedaaa9ad11f..e092d0e7c752 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 b9de7126dbf7..e73dcacc8aa7 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++}>Increment
- {count} / {test}
+ Count: {count}
+ count++}>Increment
+ {count} / {maybe_throw()}
diff --git a/packages/svelte/tests/runtime-runes/samples/error-boundary-22/Child.svelte b/packages/svelte/tests/runtime-runes/samples/error-boundary-22/Child.svelte
new file mode 100644
index 000000000000..ea60542af9cc
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/error-boundary-22/Child.svelte
@@ -0,0 +1,3 @@
+
diff --git a/packages/svelte/tests/runtime-runes/samples/error-boundary-22/_config.js b/packages/svelte/tests/runtime-runes/samples/error-boundary-22/_config.js
new file mode 100644
index 000000000000..c6be4a8cfdf8
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/error-boundary-22/_config.js
@@ -0,0 +1,11 @@
+import { flushSync } from 'svelte';
+import { test } from '../../test';
+
+export default test({
+ mode: ['client'],
+ test({ assert, target }) {
+ flushSync();
+
+ assert.htmlEqual(target.innerHTML, 'error occurred
');
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/error-boundary-22/main.svelte b/packages/svelte/tests/runtime-runes/samples/error-boundary-22/main.svelte
new file mode 100644
index 000000000000..39b2fb2eb233
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/error-boundary-22/main.svelte
@@ -0,0 +1,15 @@
+
+
+
+ This should be removed
+
+ {#if true}
+
+ {/if}
+
+ {#snippet failed()}
+ error occurred
+ {/snippet}
+
diff --git a/packages/svelte/tests/runtime-runes/samples/event-attribute-spread-update/_config.js b/packages/svelte/tests/runtime-runes/samples/event-attribute-spread-update/_config.js
new file mode 100644
index 000000000000..af03eed4c9e5
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/event-attribute-spread-update/_config.js
@@ -0,0 +1,18 @@
+import { flushSync } from 'svelte';
+import { test } from '../../test';
+
+export default test({
+ test({ assert, target }) {
+ const [change, increment] = target.querySelectorAll('button');
+
+ increment.click();
+ flushSync();
+ assert.htmlEqual(target.innerHTML, 'change handlers 1 / 1 ');
+
+ change.click();
+ flushSync();
+ increment.click();
+ flushSync();
+ assert.htmlEqual(target.innerHTML, 'change handlers 3 / 3 ');
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/event-attribute-spread-update/main.svelte b/packages/svelte/tests/runtime-runes/samples/event-attribute-spread-update/main.svelte
new file mode 100644
index 000000000000..32d4b242cc55
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/event-attribute-spread-update/main.svelte
@@ -0,0 +1,27 @@
+
+
+
+ (attrs = {
+ onclick: () => {
+ delegated += 2;
+ },
+ onclickcapture: () => {
+ non_delegated += 2;
+ }
+ })}
+>
+ change handlers
+
+{delegated} / {non_delegated}
diff --git a/packages/svelte/tests/runtime-runes/samples/event-handler-component-invalid-warning/_config.js b/packages/svelte/tests/runtime-runes/samples/event-handler-component-invalid-warning/_config.js
index 01ac3c9ad0d9..e77101066994 100644
--- a/packages/svelte/tests/runtime-runes/samples/event-handler-component-invalid-warning/_config.js
+++ b/packages/svelte/tests/runtime-runes/samples/event-handler-component-invalid-warning/_config.js
@@ -7,12 +7,8 @@ export default test({
dev: true
},
- test({ assert, target, warnings }) {
- /** @type {any} */
- let error;
-
+ test({ assert, target, warnings, errors }) {
const handler = (/** @type {any}} */ e) => {
- error = e.error;
e.stopImmediatePropagation();
};
@@ -20,9 +16,7 @@ export default test({
target.querySelector('button')?.click();
- assert.throws(() => {
- throw error;
- }, /state_unsafe_mutation/);
+ assert.include(errors[0], 'state_unsafe_mutation');
window.removeEventListener('error', handler, true);
diff --git a/packages/svelte/tests/runtime-runes/samples/event-handler-invalid-values/_config.js b/packages/svelte/tests/runtime-runes/samples/event-handler-invalid-values/_config.js
new file mode 100644
index 000000000000..7ca12af774b0
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/event-handler-invalid-values/_config.js
@@ -0,0 +1,38 @@
+import { test } from '../../test';
+
+export default test({
+ mode: ['client'],
+
+ compileOptions: {
+ dev: true
+ },
+
+ test({ assert, target, warnings, logs, errors }) {
+ const handler = (/** @type {any} */ e) => {
+ e.stopImmediatePropagation();
+ };
+
+ window.addEventListener('error', handler, true);
+
+ const [b1, b2, b3] = target.querySelectorAll('button');
+
+ b1.click();
+ assert.deepEqual(logs, []);
+ assert.deepEqual(errors, []);
+
+ b2.click();
+ assert.deepEqual(logs, ['clicked']);
+ assert.deepEqual(errors, []);
+
+ logs.length = 0;
+
+ b3.click();
+ assert.deepEqual(logs, []);
+ assert.deepEqual(warnings, [
+ '`click` handler at main.svelte:10:17 should be a function. Did you mean to add a leading `() =>`?'
+ ]);
+ assert.include(errors[0], 'is not a function');
+
+ window.removeEventListener('error', handler, true);
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/event-handler-invalid-values/main.svelte b/packages/svelte/tests/runtime-runes/samples/event-handler-invalid-values/main.svelte
new file mode 100644
index 000000000000..f6e344ece8cf
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/event-handler-invalid-values/main.svelte
@@ -0,0 +1,10 @@
+
+
+click
+click
+click
diff --git a/packages/svelte/tests/runtime-runes/samples/event-handler-invalid-warning/_config.js b/packages/svelte/tests/runtime-runes/samples/event-handler-invalid-warning/_config.js
index 423351a4c7cf..a0c792360e3b 100644
--- a/packages/svelte/tests/runtime-runes/samples/event-handler-invalid-warning/_config.js
+++ b/packages/svelte/tests/runtime-runes/samples/event-handler-invalid-warning/_config.js
@@ -7,12 +7,8 @@ export default test({
dev: true
},
- test({ assert, target, warnings }) {
- /** @type {any} */
- let error;
-
+ test({ assert, target, warnings, errors }) {
const handler = (/** @type {any} */ e) => {
- error = e.error;
e.stopImmediatePropagation();
};
@@ -20,9 +16,7 @@ export default test({
target.querySelector('button')?.click();
- assert.throws(() => {
- throw error;
- }, /state_unsafe_mutation/);
+ assert.include(errors[0], 'state_unsafe_mutation');
window.removeEventListener('error', handler, true);
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 000000000000..775afcab8913
--- /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 000000000000..6a1f9b6b4b8c
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/event-media-element-cleanup/main.svelte
@@ -0,0 +1,9 @@
+
+
+ show = false}>show/hide
+{#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 000000000000..85e06fa8ec11
--- /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 000000000000..448d495cf593
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/flush-sync-no-scheduled/main.svelte
@@ -0,0 +1,23 @@
+
+
+{
+ flushSync(() => {
+ test = !test
+ })
+
+ flag = !flag;
+}}>switch
+
+
+ {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 000000000000..5d36f2e96956
--- /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 000000000000..302a01f33502
--- /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/get-abort-signal/_config.js b/packages/svelte/tests/runtime-runes/samples/get-abort-signal/_config.js
new file mode 100644
index 000000000000..6a85e2615aab
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/get-abort-signal/_config.js
@@ -0,0 +1,34 @@
+import { test } from '../../test';
+
+export default test({
+ html: `increment loading...
`,
+
+ async test({ assert, target, variant, logs }) {
+ await new Promise((f) => setTimeout(f, 50));
+
+ if (variant === 'hydrate') {
+ assert.deepEqual(logs, [
+ 'aborted',
+ 'StaleReactionError',
+ 'The reaction that called `getAbortSignal()` was re-run or destroyed'
+ ]);
+ }
+
+ logs.length = 0;
+
+ const [button] = target.querySelectorAll('button');
+
+ await new Promise((f) => setTimeout(f, 50));
+ assert.htmlEqual(target.innerHTML, 'increment 0
');
+
+ button.click();
+ await new Promise((f) => setTimeout(f, 50));
+ assert.htmlEqual(target.innerHTML, 'increment 2
');
+
+ assert.deepEqual(logs, [
+ 'aborted',
+ 'StaleReactionError',
+ 'The reaction that called `getAbortSignal()` was re-run or destroyed'
+ ]);
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/get-abort-signal/main.svelte b/packages/svelte/tests/runtime-runes/samples/get-abort-signal/main.svelte
new file mode 100644
index 000000000000..be5625125b38
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/get-abort-signal/main.svelte
@@ -0,0 +1,33 @@
+
+
+ {
+ count += 1;
+ await Promise.resolve();
+ count += 1;
+}}>increment
+
+{#await delayed_count}
+ loading...
+{:then count}
+ {count}
+{:catch error}
+ {console.log('this should never be rendered')}
+{/await}
diff --git a/packages/svelte/tests/runtime-runes/samples/head-payload-validation/_config.js b/packages/svelte/tests/runtime-runes/samples/head-payload-validation/_config.js
new file mode 100644
index 000000000000..7c609205df9b
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/head-payload-validation/_config.js
@@ -0,0 +1,11 @@
+import { test } from '../../test';
+
+export default test({
+ compileOptions: {
+ dev: true
+ },
+ mode: ['server'],
+ async test({ errors, assert }) {
+ assert.equal(errors, []);
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/head-payload-validation/main.svelte b/packages/svelte/tests/runtime-runes/samples/head-payload-validation/main.svelte
new file mode 100644
index 000000000000..7eb31d3a9ee9
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/head-payload-validation/main.svelte
@@ -0,0 +1,7 @@
+{#snippet head()}
+ Cool
+{/snippet}
+
+
+ {@render head()}
+
\ No newline at end of file
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 000000000000..b4281bbcbd91
--- /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 000000000000..673f66891640
--- /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 000000000000..04afa7d664fe
--- /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}
+
+ { outer = false; inner = undefined; }}>Set both to falsy
+ { outer = true }}>Set outer to truthy
diff --git a/packages/svelte/tests/runtime-runes/samples/inspect-exception/_config.js b/packages/svelte/tests/runtime-runes/samples/inspect-exception/_config.js
index 57caf6e08dbc..e155ff236a48 100644
--- a/packages/svelte/tests/runtime-runes/samples/inspect-exception/_config.js
+++ b/packages/svelte/tests/runtime-runes/samples/inspect-exception/_config.js
@@ -6,11 +6,12 @@ export default test({
dev: true
},
- async test({ assert, target, logs }) {
+ async test({ assert, target, logs, errors }) {
const b1 = target.querySelector('button');
b1?.click();
flushSync();
+ assert.ok(errors.length > 0);
assert.deepEqual(logs, ['init', 'a', 'init', 'b']);
}
});
diff --git a/packages/svelte/tests/runtime-runes/samples/inspect-state-unsafe-mutation/_config.js b/packages/svelte/tests/runtime-runes/samples/inspect-state-unsafe-mutation/_config.js
new file mode 100644
index 000000000000..dcf3a8bc3d3f
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/inspect-state-unsafe-mutation/_config.js
@@ -0,0 +1,13 @@
+import { flushSync } from 'svelte';
+import { test } from '../../test';
+
+export default test({
+ compileOptions: {
+ dev: true
+ },
+
+ error: 'state_unsafe_mutation',
+
+ // silence the logs
+ test({ logs }) {}
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/inspect-state-unsafe-mutation/main.svelte b/packages/svelte/tests/runtime-runes/samples/inspect-state-unsafe-mutation/main.svelte
new file mode 100644
index 000000000000..3361087ff7ea
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/inspect-state-unsafe-mutation/main.svelte
@@ -0,0 +1,10 @@
+
\ No newline at end of file
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 000000000000..98b3fb6cbce3
--- /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 000000000000..56bd497e0955
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/inspect-trace-class/main.svelte
@@ -0,0 +1,28 @@
+
+
+
+ clicks: {counter.count}
+
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 000000000000..a22f006dcc41
--- /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 000000000000..94cd9d8aaffa
--- /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 000000000000..e89ee7d9bc03
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/inspect-trace-each/main.svelte
@@ -0,0 +1,11 @@
+
+
+ array = [{ id: 1, hi: false}]}>update
+
+{#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 f54f78f5c1f3..5957d2cc6aef 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-null/_config.js b/packages/svelte/tests/runtime-runes/samples/inspect-trace-null/_config.js
index e779a835c2ef..7982e8c1c62c 100644
--- a/packages/svelte/tests/runtime-runes/samples/inspect-trace-null/_config.js
+++ b/packages/svelte/tests/runtime-runes/samples/inspect-trace-null/_config.js
@@ -1,8 +1,12 @@
+import { assert } from 'vitest';
import { test } from '../../test';
export default test({
compileOptions: {
dev: true
},
- test() {}
+
+ test({ logs }) {
+ assert.ok(logs.length > 0);
+ }
});
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 c9a66289a12d..b4f2cf3691fd 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 efa5985e4e56..8e9204c90f2f 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/inspect-with-untracked/_config.js b/packages/svelte/tests/runtime-runes/samples/inspect-with-untracked/_config.js
new file mode 100644
index 000000000000..cdb242c416cb
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/inspect-with-untracked/_config.js
@@ -0,0 +1,20 @@
+import { flushSync } from 'svelte';
+import { test } from '../../test';
+
+export default test({
+ compileOptions: {
+ dev: true
+ },
+ async test({ assert, target, logs }) {
+ const [a, b] = target.querySelectorAll('button');
+ assert.deepEqual(logs, ['init', 0]);
+ flushSync(() => {
+ b?.click();
+ });
+ assert.deepEqual(logs, ['init', 0]);
+ flushSync(() => {
+ a?.click();
+ });
+ assert.deepEqual(logs, ['init', 0, 'update', 1]);
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/inspect-with-untracked/main.svelte b/packages/svelte/tests/runtime-runes/samples/inspect-with-untracked/main.svelte
new file mode 100644
index 000000000000..5bcf2bd3480e
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/inspect-with-untracked/main.svelte
@@ -0,0 +1,13 @@
+
+
+a++}>
+b++}>
\ No newline at end of file
diff --git a/packages/svelte/tests/runtime-runes/samples/legacy-runes-ambiguous-explicit-false/_config.js b/packages/svelte/tests/runtime-runes/samples/legacy-runes-ambiguous-explicit-false/_config.js
new file mode 100644
index 000000000000..904fea3f653c
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/legacy-runes-ambiguous-explicit-false/_config.js
@@ -0,0 +1,17 @@
+import { flushSync } from 'svelte';
+import { test } from '../../test';
+
+export default test({
+ mode: ['client'],
+ compileOptions: {
+ runes: undefined
+ },
+ async test({ assert, target }) {
+ const p = target.querySelector('p');
+ const btn = target.querySelector('button');
+ flushSync(() => {
+ btn?.click();
+ });
+ assert.equal(p?.innerHTML, '0');
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/legacy-runes-ambiguous-explicit-false/main.svelte b/packages/svelte/tests/runtime-runes/samples/legacy-runes-ambiguous-explicit-false/main.svelte
new file mode 100644
index 000000000000..cb5837dcd6da
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/legacy-runes-ambiguous-explicit-false/main.svelte
@@ -0,0 +1,8 @@
+
+
+
+{get()}
+
+set()}>
\ No newline at end of file
diff --git a/packages/svelte/tests/runtime-runes/samples/legacy-runes-ambiguous-explicit-false/test.svelte.js b/packages/svelte/tests/runtime-runes/samples/legacy-runes-ambiguous-explicit-false/test.svelte.js
new file mode 100644
index 000000000000..f0e7181c29cc
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/legacy-runes-ambiguous-explicit-false/test.svelte.js
@@ -0,0 +1,9 @@
+let count = $state(0);
+
+export function get() {
+ return count;
+}
+
+export function set() {
+ count++;
+}
diff --git a/packages/svelte/tests/runtime-runes/samples/legacy-runes-ambiguous-export-$$props/_config.js b/packages/svelte/tests/runtime-runes/samples/legacy-runes-ambiguous-export-$$props/_config.js
new file mode 100644
index 000000000000..904fea3f653c
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/legacy-runes-ambiguous-export-$$props/_config.js
@@ -0,0 +1,17 @@
+import { flushSync } from 'svelte';
+import { test } from '../../test';
+
+export default test({
+ mode: ['client'],
+ compileOptions: {
+ runes: undefined
+ },
+ async test({ assert, target }) {
+ const p = target.querySelector('p');
+ const btn = target.querySelector('button');
+ flushSync(() => {
+ btn?.click();
+ });
+ assert.equal(p?.innerHTML, '0');
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/legacy-runes-ambiguous-export-$$props/main.svelte b/packages/svelte/tests/runtime-runes/samples/legacy-runes-ambiguous-export-$$props/main.svelte
new file mode 100644
index 000000000000..a7370b48d6d2
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/legacy-runes-ambiguous-export-$$props/main.svelte
@@ -0,0 +1,9 @@
+
+
+{get()}
+
+set()}>
\ No newline at end of file
diff --git a/packages/svelte/tests/runtime-runes/samples/legacy-runes-ambiguous-export-$$props/test.svelte.js b/packages/svelte/tests/runtime-runes/samples/legacy-runes-ambiguous-export-$$props/test.svelte.js
new file mode 100644
index 000000000000..f0e7181c29cc
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/legacy-runes-ambiguous-export-$$props/test.svelte.js
@@ -0,0 +1,9 @@
+let count = $state(0);
+
+export function get() {
+ return count;
+}
+
+export function set() {
+ count++;
+}
diff --git a/packages/svelte/tests/runtime-runes/samples/legacy-runes-ambiguous-export-$$restProps/_config.js b/packages/svelte/tests/runtime-runes/samples/legacy-runes-ambiguous-export-$$restProps/_config.js
new file mode 100644
index 000000000000..904fea3f653c
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/legacy-runes-ambiguous-export-$$restProps/_config.js
@@ -0,0 +1,17 @@
+import { flushSync } from 'svelte';
+import { test } from '../../test';
+
+export default test({
+ mode: ['client'],
+ compileOptions: {
+ runes: undefined
+ },
+ async test({ assert, target }) {
+ const p = target.querySelector('p');
+ const btn = target.querySelector('button');
+ flushSync(() => {
+ btn?.click();
+ });
+ assert.equal(p?.innerHTML, '0');
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/legacy-runes-ambiguous-export-$$restProps/main.svelte b/packages/svelte/tests/runtime-runes/samples/legacy-runes-ambiguous-export-$$restProps/main.svelte
new file mode 100644
index 000000000000..ccf9bd28642c
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/legacy-runes-ambiguous-export-$$restProps/main.svelte
@@ -0,0 +1,9 @@
+
+
+{get()}
+
+set()}>
\ No newline at end of file
diff --git a/packages/svelte/tests/runtime-runes/samples/legacy-runes-ambiguous-export-$$restProps/test.svelte.js b/packages/svelte/tests/runtime-runes/samples/legacy-runes-ambiguous-export-$$restProps/test.svelte.js
new file mode 100644
index 000000000000..f0e7181c29cc
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/legacy-runes-ambiguous-export-$$restProps/test.svelte.js
@@ -0,0 +1,9 @@
+let count = $state(0);
+
+export function get() {
+ return count;
+}
+
+export function set() {
+ count++;
+}
diff --git a/packages/svelte/tests/runtime-runes/samples/legacy-runes-ambiguous-export-const/_config.js b/packages/svelte/tests/runtime-runes/samples/legacy-runes-ambiguous-export-const/_config.js
new file mode 100644
index 000000000000..2d72de6d134d
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/legacy-runes-ambiguous-export-const/_config.js
@@ -0,0 +1,17 @@
+import { flushSync } from 'svelte';
+import { test } from '../../test';
+
+export default test({
+ mode: ['client'],
+ compileOptions: {
+ runes: undefined
+ },
+ async test({ assert, target }) {
+ const p = target.querySelector('p');
+ const btn = target.querySelector('button');
+ flushSync(() => {
+ btn?.click();
+ });
+ assert.equal(p?.innerHTML, '1');
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/legacy-runes-ambiguous-export-const/main.svelte b/packages/svelte/tests/runtime-runes/samples/legacy-runes-ambiguous-export-const/main.svelte
new file mode 100644
index 000000000000..49aef8151845
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/legacy-runes-ambiguous-export-const/main.svelte
@@ -0,0 +1,9 @@
+
+
+{get()}
+
+set()}>
\ No newline at end of file
diff --git a/packages/svelte/tests/runtime-runes/samples/legacy-runes-ambiguous-export-const/test.svelte.js b/packages/svelte/tests/runtime-runes/samples/legacy-runes-ambiguous-export-const/test.svelte.js
new file mode 100644
index 000000000000..f0e7181c29cc
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/legacy-runes-ambiguous-export-const/test.svelte.js
@@ -0,0 +1,9 @@
+let count = $state(0);
+
+export function get() {
+ return count;
+}
+
+export function set() {
+ count++;
+}
diff --git a/packages/svelte/tests/runtime-runes/samples/legacy-runes-ambiguous-export-labeled/_config.js b/packages/svelte/tests/runtime-runes/samples/legacy-runes-ambiguous-export-labeled/_config.js
new file mode 100644
index 000000000000..c4cff665d22b
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/legacy-runes-ambiguous-export-labeled/_config.js
@@ -0,0 +1,17 @@
+import { flushSync } from 'svelte';
+import { test } from '../../test';
+
+export default test({
+ mode: ['client'],
+ compileOptions: {
+ runes: undefined
+ },
+ async test({ assert, target, logs }) {
+ const p = target.querySelector('p');
+ const btn = target.querySelector('button');
+ flushSync(() => {
+ btn?.click();
+ });
+ assert.equal(p?.innerHTML, '0');
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/legacy-runes-ambiguous-export-labeled/main.svelte b/packages/svelte/tests/runtime-runes/samples/legacy-runes-ambiguous-export-labeled/main.svelte
new file mode 100644
index 000000000000..f7cd0ede2e39
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/legacy-runes-ambiguous-export-labeled/main.svelte
@@ -0,0 +1,9 @@
+
+
+{get()}
+
+set()}>
\ No newline at end of file
diff --git a/packages/svelte/tests/runtime-runes/samples/legacy-runes-ambiguous-export-labeled/test.svelte.js b/packages/svelte/tests/runtime-runes/samples/legacy-runes-ambiguous-export-labeled/test.svelte.js
new file mode 100644
index 000000000000..f0e7181c29cc
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/legacy-runes-ambiguous-export-labeled/test.svelte.js
@@ -0,0 +1,9 @@
+let count = $state(0);
+
+export function get() {
+ return count;
+}
+
+export function set() {
+ count++;
+}
diff --git a/packages/svelte/tests/runtime-runes/samples/legacy-runes-ambiguous-export-let-2/_config.js b/packages/svelte/tests/runtime-runes/samples/legacy-runes-ambiguous-export-let-2/_config.js
new file mode 100644
index 000000000000..904fea3f653c
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/legacy-runes-ambiguous-export-let-2/_config.js
@@ -0,0 +1,17 @@
+import { flushSync } from 'svelte';
+import { test } from '../../test';
+
+export default test({
+ mode: ['client'],
+ compileOptions: {
+ runes: undefined
+ },
+ async test({ assert, target }) {
+ const p = target.querySelector('p');
+ const btn = target.querySelector('button');
+ flushSync(() => {
+ btn?.click();
+ });
+ assert.equal(p?.innerHTML, '0');
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/legacy-runes-ambiguous-export-let-2/main.svelte b/packages/svelte/tests/runtime-runes/samples/legacy-runes-ambiguous-export-let-2/main.svelte
new file mode 100644
index 000000000000..36becde4a300
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/legacy-runes-ambiguous-export-let-2/main.svelte
@@ -0,0 +1,10 @@
+
+
+{x}
+{get()}
+
+set()}>
\ No newline at end of file
diff --git a/packages/svelte/tests/runtime-runes/samples/legacy-runes-ambiguous-export-let-2/test.svelte.js b/packages/svelte/tests/runtime-runes/samples/legacy-runes-ambiguous-export-let-2/test.svelte.js
new file mode 100644
index 000000000000..f0e7181c29cc
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/legacy-runes-ambiguous-export-let-2/test.svelte.js
@@ -0,0 +1,9 @@
+let count = $state(0);
+
+export function get() {
+ return count;
+}
+
+export function set() {
+ count++;
+}
diff --git a/packages/svelte/tests/runtime-runes/samples/legacy-runes-ambiguous-export-let/_config.js b/packages/svelte/tests/runtime-runes/samples/legacy-runes-ambiguous-export-let/_config.js
new file mode 100644
index 000000000000..904fea3f653c
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/legacy-runes-ambiguous-export-let/_config.js
@@ -0,0 +1,17 @@
+import { flushSync } from 'svelte';
+import { test } from '../../test';
+
+export default test({
+ mode: ['client'],
+ compileOptions: {
+ runes: undefined
+ },
+ async test({ assert, target }) {
+ const p = target.querySelector('p');
+ const btn = target.querySelector('button');
+ flushSync(() => {
+ btn?.click();
+ });
+ assert.equal(p?.innerHTML, '0');
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/legacy-runes-ambiguous-export-let/main.svelte b/packages/svelte/tests/runtime-runes/samples/legacy-runes-ambiguous-export-let/main.svelte
new file mode 100644
index 000000000000..1449b7d5824c
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/legacy-runes-ambiguous-export-let/main.svelte
@@ -0,0 +1,12 @@
+
+
+{x}
+{get()}
+
+set()}>
\ No newline at end of file
diff --git a/packages/svelte/tests/runtime-runes/samples/legacy-runes-ambiguous-export-let/test.svelte.js b/packages/svelte/tests/runtime-runes/samples/legacy-runes-ambiguous-export-let/test.svelte.js
new file mode 100644
index 000000000000..f0e7181c29cc
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/legacy-runes-ambiguous-export-let/test.svelte.js
@@ -0,0 +1,9 @@
+let count = $state(0);
+
+export function get() {
+ return count;
+}
+
+export function set() {
+ count++;
+}
diff --git a/packages/svelte/tests/runtime-runes/samples/legacy-runes-ambiguous/_config.js b/packages/svelte/tests/runtime-runes/samples/legacy-runes-ambiguous/_config.js
new file mode 100644
index 000000000000..2d72de6d134d
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/legacy-runes-ambiguous/_config.js
@@ -0,0 +1,17 @@
+import { flushSync } from 'svelte';
+import { test } from '../../test';
+
+export default test({
+ mode: ['client'],
+ compileOptions: {
+ runes: undefined
+ },
+ async test({ assert, target }) {
+ const p = target.querySelector('p');
+ const btn = target.querySelector('button');
+ flushSync(() => {
+ btn?.click();
+ });
+ assert.equal(p?.innerHTML, '1');
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/legacy-runes-ambiguous/main.svelte b/packages/svelte/tests/runtime-runes/samples/legacy-runes-ambiguous/main.svelte
new file mode 100644
index 000000000000..698a852ac927
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/legacy-runes-ambiguous/main.svelte
@@ -0,0 +1,7 @@
+
+
+{get()}
+
+set()}>
\ No newline at end of file
diff --git a/packages/svelte/tests/runtime-runes/samples/legacy-runes-ambiguous/test.svelte.js b/packages/svelte/tests/runtime-runes/samples/legacy-runes-ambiguous/test.svelte.js
new file mode 100644
index 000000000000..f0e7181c29cc
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/legacy-runes-ambiguous/test.svelte.js
@@ -0,0 +1,9 @@
+let count = $state(0);
+
+export function get() {
+ return count;
+}
+
+export function set() {
+ count++;
+}
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 f7a4ca05f570..d8b202955aae 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 446a9213dda2..fe07ed8ab0d0 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/nested-effect-conflict/_config.js b/packages/svelte/tests/runtime-runes/samples/nested-effect-conflict/_config.js
index a8c16b7008c9..eb631bc9f4bc 100644
--- a/packages/svelte/tests/runtime-runes/samples/nested-effect-conflict/_config.js
+++ b/packages/svelte/tests/runtime-runes/samples/nested-effect-conflict/_config.js
@@ -10,14 +10,6 @@ export default test({
});
await Promise.resolve();
- assert.deepEqual(logs, [
- 'top level',
- 'inner',
- 0,
- 'destroy inner',
- undefined,
- 'destroy outer',
- undefined
- ]);
+ assert.deepEqual(logs, ['top level', 'inner', 0, 'destroy inner', 0, 'destroy outer', 0]);
}
});
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 000000000000..5bbe4483d350
--- /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 000000000000..f71a5e6c43bb
--- /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/non-local-mutation-discouraged/_config.js b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-discouraged/_config.js
index 62c696124285..84526610260d 100644
--- a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-discouraged/_config.js
+++ b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-discouraged/_config.js
@@ -10,7 +10,7 @@ export default test({
test({ assert, target, warnings }) {
const warning =
- 'Counter.svelte mutated a value owned by main.svelte. This is strongly discouraged. Consider passing values to child components with `bind:`, or use a callback instead';
+ 'Mutating unbound props (`object`, at Counter.svelte:5:23) is strongly discouraged. Consider using `bind:object={...}` in main.svelte (or using a callback) instead';
const [btn1, btn2] = target.querySelectorAll('button');
btn1.click();
diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-global-2/child.svelte b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-global-2/child.svelte
deleted file mode 100644
index 13de75364752..000000000000
--- a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-global-2/child.svelte
+++ /dev/null
@@ -1,18 +0,0 @@
-
diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-global-2/main.svelte b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-global-2/main.svelte
deleted file mode 100644
index 8a6922e9e250..000000000000
--- a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-global-2/main.svelte
+++ /dev/null
@@ -1,5 +0,0 @@
-
diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner/_config.js b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-1/_config.js
similarity index 74%
rename from packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner/_config.js
rename to packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-1/_config.js
index c07b9ce129dc..96b18d1854c8 100644
--- a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner/_config.js
+++ b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-1/_config.js
@@ -8,7 +8,7 @@ let warn;
let warnings = [];
export default test({
- html: `clicks: 0 `,
+ html: `[] `,
compileOptions: {
dev: true
@@ -34,8 +34,8 @@ export default test({
btn?.click();
});
- assert.htmlEqual(target.innerHTML, `clicks: 1 `);
+ assert.htmlEqual(target.innerHTML, `[foo] `);
- assert.deepEqual(warnings, []);
+ assert.deepEqual(warnings, [], 'expected getContext to have widened ownership');
}
});
diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-1/main.svelte b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-1/main.svelte
new file mode 100644
index 000000000000..2dd7cab141d6
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-1/main.svelte
@@ -0,0 +1,9 @@
+
+
+
diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-3/sub.svelte b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-1/sub.svelte
similarity index 100%
rename from packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-3/sub.svelte
rename to packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-1/sub.svelte
diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-2/_config.js b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-2/_config.js
index c07b9ce129dc..66f1726a2aef 100644
--- a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-2/_config.js
+++ b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-2/_config.js
@@ -1,41 +1,24 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
-/** @type {typeof console.warn} */
-let warn;
-
-/** @type {any[]} */
-let warnings = [];
-
export default test({
- html: `clicks: 0 `,
-
compileOptions: {
dev: true
},
- before_test: () => {
- warn = console.warn;
-
- console.warn = (...args) => {
- warnings.push(...args);
- };
- },
+ test({ assert, target, warnings }) {
+ const [btn1, btn2] = target.querySelectorAll('button');
- after_test: () => {
- console.warn = warn;
- warnings = [];
- },
+ flushSync(() => {
+ btn1.click();
+ });
- test({ assert, target }) {
- const btn = target.querySelector('button');
+ assert.deepEqual(warnings.length, 0);
flushSync(() => {
- btn?.click();
+ btn2.click();
});
- assert.htmlEqual(target.innerHTML, `clicks: 1 `);
-
- assert.deepEqual(warnings, []);
+ assert.deepEqual(warnings.length, 1);
}
});
diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-2/main.svelte b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-2/main.svelte
index ad450a937e40..0be7e434e475 100644
--- a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-2/main.svelte
+++ b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-2/main.svelte
@@ -1,9 +1,8 @@
-
- global.a.b += 1}>
- clicks: {global.a.b}
-
+
diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-2/state.svelte.js b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-2/state.svelte.js
index 3e7a68cf97d8..2906b9bce52b 100644
--- a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-2/state.svelte.js
+++ b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-2/state.svelte.js
@@ -1 +1,14 @@
-export let global = $state({});
+export function create_my_state() {
+ const my_state = $state({
+ a: 0
+ });
+
+ function inc() {
+ my_state.a++;
+ }
+
+ return {
+ my_state,
+ inc
+ };
+}
diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-5/sub.svelte b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-2/sub.svelte
similarity index 100%
rename from packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-5/sub.svelte
rename to packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-2/sub.svelte
diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-7/Child.svelte b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-3/Child.svelte
similarity index 100%
rename from packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-7/Child.svelte
rename to packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-3/Child.svelte
diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-3/_config.js b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-3/_config.js
index 96b18d1854c8..ab7327ab8b82 100644
--- a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-3/_config.js
+++ b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-3/_config.js
@@ -1,41 +1,24 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
-/** @type {typeof console.warn} */
-let warn;
-
-/** @type {any[]} */
-let warnings = [];
-
export default test({
- html: `[] `,
-
compileOptions: {
dev: true
},
- before_test: () => {
- warn = console.warn;
-
- console.warn = (...args) => {
- warnings.push(...args);
- };
- },
+ async test({ assert, target, warnings }) {
+ const [btn1, btn2] = target.querySelectorAll('button');
- after_test: () => {
- console.warn = warn;
- warnings = [];
- },
+ flushSync(() => {
+ btn1.click();
+ });
- test({ assert, target }) {
- const btn = target.querySelector('button');
+ assert.deepEqual(warnings.length, 0);
flushSync(() => {
- btn?.click();
+ btn2.click();
});
- assert.htmlEqual(target.innerHTML, `[foo] `);
-
- assert.deepEqual(warnings, [], 'expected getContext to have widened ownership');
+ assert.deepEqual(warnings.length, 1);
}
});
diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-3/main.svelte b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-3/main.svelte
index 2dd7cab141d6..8e8343790b1b 100644
--- a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-3/main.svelte
+++ b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-3/main.svelte
@@ -1,9 +1,13 @@
-
+First click here
+
diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-4/_config.js b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-4/_config.js
deleted file mode 100644
index aeb3740dfe71..000000000000
--- a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-4/_config.js
+++ /dev/null
@@ -1,37 +0,0 @@
-import { flushSync } from 'svelte';
-import { test } from '../../test';
-
-/** @type {typeof console.warn} */
-let warn;
-
-/** @type {any[]} */
-let warnings = [];
-
-export default test({
- compileOptions: {
- dev: true
- },
-
- before_test: () => {
- warn = console.warn;
-
- console.warn = (...args) => {
- warnings.push(...args);
- };
- },
-
- after_test: () => {
- console.warn = warn;
- warnings = [];
- },
-
- test({ assert, target }) {
- const btn = target.querySelector('button');
-
- flushSync(() => {
- btn?.click();
- });
-
- assert.deepEqual(warnings, []);
- }
-});
diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-4/main.svelte b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-4/main.svelte
deleted file mode 100644
index 2d40c13949a6..000000000000
--- a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-4/main.svelte
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-
-
- global.increment_a_b()}>
- click me
-
diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-4/state.svelte.js b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-4/state.svelte.js
deleted file mode 100644
index 40790591712c..000000000000
--- a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-4/state.svelte.js
+++ /dev/null
@@ -1,13 +0,0 @@
-class Global {
- state = $state({});
-
- add_a(a) {
- this.state.a = a;
- }
-
- increment_a_b() {
- this.state.a.b++;
- }
-}
-
-export const global = new Global();
diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-4/sub.svelte b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-4/sub.svelte
deleted file mode 100644
index 044904aa187e..000000000000
--- a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-4/sub.svelte
+++ /dev/null
@@ -1,8 +0,0 @@
-
diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-5/_config.js b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-5/_config.js
deleted file mode 100644
index 66f1726a2aef..000000000000
--- a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-5/_config.js
+++ /dev/null
@@ -1,24 +0,0 @@
-import { flushSync } from 'svelte';
-import { test } from '../../test';
-
-export default test({
- compileOptions: {
- dev: true
- },
-
- test({ assert, target, warnings }) {
- const [btn1, btn2] = target.querySelectorAll('button');
-
- flushSync(() => {
- btn1.click();
- });
-
- assert.deepEqual(warnings.length, 0);
-
- flushSync(() => {
- btn2.click();
- });
-
- assert.deepEqual(warnings.length, 1);
- }
-});
diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-5/main.svelte b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-5/main.svelte
deleted file mode 100644
index 0be7e434e475..000000000000
--- a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-5/main.svelte
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-5/state.svelte.js b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-5/state.svelte.js
deleted file mode 100644
index 2906b9bce52b..000000000000
--- a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-5/state.svelte.js
+++ /dev/null
@@ -1,14 +0,0 @@
-export function create_my_state() {
- const my_state = $state({
- a: 0
- });
-
- function inc() {
- my_state.a++;
- }
-
- return {
- my_state,
- inc
- };
-}
diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-6/Child.svelte b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-6/Child.svelte
deleted file mode 100644
index aa31fd7606dd..000000000000
--- a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-6/Child.svelte
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
- {
- foo.person.name.last = 'T';
-}}>mutate
diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-6/_config.js b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-6/_config.js
deleted file mode 100644
index cc9ea715f0aa..000000000000
--- a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-6/_config.js
+++ /dev/null
@@ -1,37 +0,0 @@
-import { flushSync } from 'svelte';
-import { test } from '../../test';
-
-/** @type {typeof console.warn} */
-let warn;
-
-/** @type {any[]} */
-let warnings = [];
-
-export default test({
- compileOptions: {
- dev: true
- },
-
- before_test: () => {
- warn = console.warn;
-
- console.warn = (...args) => {
- warnings.push(...args);
- };
- },
-
- after_test: () => {
- console.warn = warn;
- warnings = [];
- },
-
- async test({ assert, target }) {
- const btn = target.querySelector('button');
-
- flushSync(() => {
- btn?.click();
- });
-
- assert.deepEqual(warnings.length, 0);
- }
-});
diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-6/main.svelte b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-6/main.svelte
deleted file mode 100644
index 92d7dbd2db6c..000000000000
--- a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-6/main.svelte
+++ /dev/null
@@ -1,17 +0,0 @@
-
-
-
diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-7/_config.js b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-7/_config.js
deleted file mode 100644
index ab7327ab8b82..000000000000
--- a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-7/_config.js
+++ /dev/null
@@ -1,24 +0,0 @@
-import { flushSync } from 'svelte';
-import { test } from '../../test';
-
-export default test({
- compileOptions: {
- dev: true
- },
-
- async test({ assert, target, warnings }) {
- const [btn1, btn2] = target.querySelectorAll('button');
-
- flushSync(() => {
- btn1.click();
- });
-
- assert.deepEqual(warnings.length, 0);
-
- flushSync(() => {
- btn2.click();
- });
-
- assert.deepEqual(warnings.length, 1);
- }
-});
diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-7/main.svelte b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-7/main.svelte
deleted file mode 100644
index 8e8343790b1b..000000000000
--- a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-7/main.svelte
+++ /dev/null
@@ -1,13 +0,0 @@
-
-
-First click here
-
diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner/Counter.svelte b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner/Counter.svelte
deleted file mode 100644
index ffe6ef75c4ed..000000000000
--- a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner/Counter.svelte
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
- global.object.count += 1}>
- clicks: {global.object.count}
-
diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner/main.svelte b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner/main.svelte
deleted file mode 100644
index 5f1c7461f636..000000000000
--- a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner/main.svelte
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner/state.svelte.js b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner/state.svelte.js
deleted file mode 100644
index 6881c2faf66b..000000000000
--- a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner/state.svelte.js
+++ /dev/null
@@ -1,3 +0,0 @@
-export let global = $state({
- object: { count: -1 }
-});
diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-ok/_config.js b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-ok/_config.js
new file mode 100644
index 000000000000..437385e185c8
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-ok/_config.js
@@ -0,0 +1,18 @@
+import { flushSync } from 'svelte';
+import { test } from '../../test';
+
+export default test({
+ compileOptions: {
+ dev: true
+ },
+
+ test({ assert, target, warnings }) {
+ const btn = target.querySelector('button');
+ btn?.click();
+ flushSync();
+
+ assert.deepEqual(warnings, []);
+ },
+
+ warnings: []
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-ok/child.svelte b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-ok/child.svelte
new file mode 100644
index 000000000000..0243a6c7d1fa
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-ok/child.svelte
@@ -0,0 +1,8 @@
+
+
+ {
+ klass.y = 2;
+ getter_setter.y = 2;
+}}>mutate
diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-ok/main.svelte b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-ok/main.svelte
new file mode 100644
index 000000000000..8685664ab164
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-ok/main.svelte
@@ -0,0 +1,21 @@
+
+
+
diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-with-binding-2/_config.js b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-with-binding-2/_config.js
index 87474a05cc33..39fa80c55a4e 100644
--- a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-with-binding-2/_config.js
+++ b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-with-binding-2/_config.js
@@ -8,6 +8,6 @@ export default test({
},
warnings: [
- 'Intermediate.svelte passed a value to Counter.svelte with `bind:`, but the value is owned by main.svelte. Consider creating a binding between main.svelte and Intermediate.svelte'
+ 'Intermediate.svelte passed property `object` to Counter.svelte with `bind:`, but its parent component main.svelte did not declare `object` as a binding. Consider creating a binding between main.svelte and Intermediate.svelte (e.g. `bind:object={...}` instead of `object={...}`)'
]
});
diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-with-binding-3/_config.js b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-with-binding-3/_config.js
index 66e51843808e..7b8cc676d528 100644
--- a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-with-binding-3/_config.js
+++ b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-with-binding-3/_config.js
@@ -33,7 +33,7 @@ export default test({
assert.htmlEqual(target.innerHTML, `clicks: 1 clicks: 1 `);
assert.deepEqual(warnings, [
- 'Counter.svelte mutated a value owned by main.svelte. This is strongly discouraged. Consider passing values to child components with `bind:`, or use a callback instead'
+ 'Mutating unbound props (`notshared`, at Counter.svelte:10:23) is strongly discouraged. Consider using `bind:notshared={...}` in main.svelte (or using a callback) instead'
]);
}
});
diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-with-binding-7/_config.js b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-with-binding-7/_config.js
index e766a946d0dc..bd2ecc28b6f7 100644
--- a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-with-binding-7/_config.js
+++ b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-with-binding-7/_config.js
@@ -1,7 +1,6 @@
import { flushSync } from 'svelte';
import { ok, test } from '../../test';
-// Tests that proxies widen ownership correctly even if not directly connected to each other
export default test({
compileOptions: {
dev: true
diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-with-binding-8/CounterBinding.svelte b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-with-binding-8/CounterBinding.svelte
deleted file mode 100644
index d6da559fb176..000000000000
--- a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-with-binding-8/CounterBinding.svelte
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-Binding
- linked3.count++}>Increment Linked 1 ({linked3.count})
- linked4.count++}>Increment Linked 2 ({linked4.count})
diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-with-binding-8/CounterContext.svelte b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-with-binding-8/CounterContext.svelte
deleted file mode 100644
index b935f0a472dc..000000000000
--- a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-with-binding-8/CounterContext.svelte
+++ /dev/null
@@ -1,13 +0,0 @@
-
-
-Context
- linked1.linked.current.count++}
- >Increment Linked 1 ({linked1.linked.current.count})
- linked2.linked.current.count++}
- >Increment Linked 2 ({linked2.linked.current.count})
diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-with-binding-8/_config.js b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-with-binding-8/_config.js
deleted file mode 100644
index d6d12d01cd09..000000000000
--- a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-with-binding-8/_config.js
+++ /dev/null
@@ -1,34 +0,0 @@
-import { flushSync } from 'svelte';
-import { test } from '../../test';
-
-// Tests that ownership is widened with $derived (on class or on its own) that contains $state
-export default test({
- compileOptions: {
- dev: true
- },
-
- test({ assert, target, warnings }) {
- const [root, counter_context1, counter_context2, counter_binding1, counter_binding2] =
- target.querySelectorAll('button');
-
- counter_context1.click();
- counter_context2.click();
- counter_binding1.click();
- counter_binding2.click();
- flushSync();
-
- assert.equal(warnings.length, 0);
-
- root.click();
- flushSync();
- counter_context1.click();
- counter_context2.click();
- counter_binding1.click();
- counter_binding2.click();
- flushSync();
-
- assert.equal(warnings.length, 0);
- },
-
- warnings: []
-});
diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-with-binding-8/main.svelte b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-with-binding-8/main.svelte
deleted file mode 100644
index aaade26e162c..000000000000
--- a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-with-binding-8/main.svelte
+++ /dev/null
@@ -1,46 +0,0 @@
-
-
-Parent
- counter.count++}>
- Increment Original ({counter.count})
-
-
-
-
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 000000000000..84e97e9735ba
--- /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 000000000000..efe39b91fecd
--- /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/onmount-prop-access/Component.svelte b/packages/svelte/tests/runtime-runes/samples/onmount-prop-access/Component.svelte
new file mode 100644
index 000000000000..b5da702fa7b2
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/onmount-prop-access/Component.svelte
@@ -0,0 +1,12 @@
+
diff --git a/packages/svelte/tests/runtime-runes/samples/onmount-prop-access/_config.js b/packages/svelte/tests/runtime-runes/samples/onmount-prop-access/_config.js
new file mode 100644
index 000000000000..0d24e265d3ab
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/onmount-prop-access/_config.js
@@ -0,0 +1,7 @@
+import { test } from '../../test';
+
+export default test({
+ async test({ assert, logs }) {
+ assert.deepEqual(logs, [1]);
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/onmount-prop-access/main.svelte b/packages/svelte/tests/runtime-runes/samples/onmount-prop-access/main.svelte
new file mode 100644
index 000000000000..92746760a484
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/onmount-prop-access/main.svelte
@@ -0,0 +1,14 @@
+
+
+{#key key}
+
+{/key}
diff --git a/packages/svelte/tests/runtime-runes/samples/ownership-invalid-binding-bindable-fallback/Child.svelte b/packages/svelte/tests/runtime-runes/samples/ownership-invalid-binding-bindable-fallback/Child.svelte
new file mode 100644
index 000000000000..78b82caed9ed
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/ownership-invalid-binding-bindable-fallback/Child.svelte
@@ -0,0 +1,5 @@
+
+
+{test}
diff --git a/packages/svelte/tests/runtime-runes/samples/ownership-invalid-binding-bindable-fallback/Parent.svelte b/packages/svelte/tests/runtime-runes/samples/ownership-invalid-binding-bindable-fallback/Parent.svelte
new file mode 100644
index 000000000000..7bfb17aa6465
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/ownership-invalid-binding-bindable-fallback/Parent.svelte
@@ -0,0 +1,7 @@
+
+
+
diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-global-2/_config.js b/packages/svelte/tests/runtime-runes/samples/ownership-invalid-binding-bindable-fallback/_config.js
similarity index 70%
rename from packages/svelte/tests/runtime-runes/samples/non-local-mutation-global-2/_config.js
rename to packages/svelte/tests/runtime-runes/samples/ownership-invalid-binding-bindable-fallback/_config.js
index b4864154c39a..e93067eb9d34 100644
--- a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-global-2/_config.js
+++ b/packages/svelte/tests/runtime-runes/samples/ownership-invalid-binding-bindable-fallback/_config.js
@@ -1,11 +1,11 @@
import { test } from '../../test';
export default test({
+ mode: ['client'],
compileOptions: {
dev: true
},
-
- async test({ assert, warnings }) {
+ async test({ warnings, assert }) {
assert.deepEqual(warnings, []);
}
});
diff --git a/packages/svelte/tests/runtime-runes/samples/ownership-invalid-binding-bindable-fallback/main.svelte b/packages/svelte/tests/runtime-runes/samples/ownership-invalid-binding-bindable-fallback/main.svelte
new file mode 100644
index 000000000000..282afb1771b0
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/ownership-invalid-binding-bindable-fallback/main.svelte
@@ -0,0 +1,5 @@
+
+
+
diff --git a/packages/svelte/tests/runtime-runes/samples/ownership-invalid-mutation-bindable-fallback/Parent.svelte b/packages/svelte/tests/runtime-runes/samples/ownership-invalid-mutation-bindable-fallback/Parent.svelte
new file mode 100644
index 000000000000..7d6b248da736
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/ownership-invalid-mutation-bindable-fallback/Parent.svelte
@@ -0,0 +1,8 @@
+
+
+test = {}}>
+test.test = {}}>
+
+{test}
\ No newline at end of file
diff --git a/packages/svelte/tests/runtime-runes/samples/ownership-invalid-mutation-bindable-fallback/_config.js b/packages/svelte/tests/runtime-runes/samples/ownership-invalid-mutation-bindable-fallback/_config.js
new file mode 100644
index 000000000000..9b4e3479ea8c
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/ownership-invalid-mutation-bindable-fallback/_config.js
@@ -0,0 +1,23 @@
+import { flushSync } from 'svelte';
+import { test } from '../../test';
+
+export default test({
+ mode: ['client'],
+ compileOptions: {
+ dev: true
+ },
+ async test({ warnings, assert, target }) {
+ const [btn, btn2] = target.querySelectorAll('button');
+ flushSync(() => {
+ btn2.click();
+ });
+ assert.deepEqual(warnings, []);
+ flushSync(() => {
+ btn.click();
+ });
+ flushSync(() => {
+ btn2.click();
+ });
+ assert.deepEqual(warnings, []);
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/ownership-invalid-mutation-bindable-fallback/main.svelte b/packages/svelte/tests/runtime-runes/samples/ownership-invalid-mutation-bindable-fallback/main.svelte
new file mode 100644
index 000000000000..282afb1771b0
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/ownership-invalid-mutation-bindable-fallback/main.svelte
@@ -0,0 +1,5 @@
+
+
+
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 000000000000..95556f473784
--- /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 000000000000..d834551221a1
--- /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 000000000000..a2e7d6d8a46a
--- /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 000000000000..3f6fb4143c27
--- /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 000000000000..3535da713272
--- /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/proxy-coercive-assignment-warning/_config.js b/packages/svelte/tests/runtime-runes/samples/proxy-coercive-assignment-warning/_config.js
index f0292274725e..4462f492fac9 100644
--- a/packages/svelte/tests/runtime-runes/samples/proxy-coercive-assignment-warning/_config.js
+++ b/packages/svelte/tests/runtime-runes/samples/proxy-coercive-assignment-warning/_config.js
@@ -6,7 +6,7 @@ export default test({
dev: true
},
- html: `items: null x
`,
+ html: `items: null x
`,
test({ assert, target, warnings }) {
const btn = target.querySelector('button');
@@ -15,13 +15,13 @@ export default test({
flushSync(() => btn.click());
assert.htmlEqual(
target.innerHTML,
- `items: [] x
`
+ `items: [] x
`
);
flushSync(() => btn.click());
assert.htmlEqual(
target.innerHTML,
- `items: [0] x
`
+ `items: [0] x
`
);
const input = target.querySelector('input');
@@ -30,7 +30,7 @@ export default test({
flushSync(() => input.dispatchEvent(new Event('change', { bubbles: true })));
assert.deepEqual(warnings, [
- 'Assignment to `items` property (main.svelte:8:24) will evaluate to the right-hand side, not the value of `items` following the assignment. This may result in unexpected behaviour.'
+ 'Assignment to `items` property (main.svelte:9:24) will evaluate to the right-hand side, not the value of `items` following the assignment. This may result in unexpected behaviour.'
]);
}
});
diff --git a/packages/svelte/tests/runtime-runes/samples/proxy-coercive-assignment-warning/main.svelte b/packages/svelte/tests/runtime-runes/samples/proxy-coercive-assignment-warning/main.svelte
index ad94c4e56e03..a79fe873b7e9 100644
--- a/packages/svelte/tests/runtime-runes/samples/proxy-coercive-assignment-warning/main.svelte
+++ b/packages/svelte/tests/runtime-runes/samples/proxy-coercive-assignment-warning/main.svelte
@@ -3,6 +3,7 @@
let entries = $state([]);
let object = $state({ items: null, group: [] });
+ let elementFunBind = $state();
(object.items ??= []).push(object.items.length)}>
@@ -13,6 +14,12 @@
x
+
entries[2], (v) => (entries[2] = v)}>
+
+{#snippet funBind(context)}
+ {}, (e) => (context.element = e)} />
+{/snippet}
+{@render funBind({ set element(e) { elementFunBind = e } })}
diff --git a/packages/svelte/tests/runtime-runes/samples/proxy-set-with-parent/_config.js b/packages/svelte/tests/runtime-runes/samples/proxy-set-with-parent/_config.js
new file mode 100644
index 000000000000..2e4a27cf0912
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/proxy-set-with-parent/_config.js
@@ -0,0 +1,5 @@
+import { test } from '../../test';
+
+export default test({
+ async test() {}
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/proxy-set-with-parent/main.svelte b/packages/svelte/tests/runtime-runes/samples/proxy-set-with-parent/main.svelte
new file mode 100644
index 000000000000..7450eff3faa2
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/proxy-set-with-parent/main.svelte
@@ -0,0 +1,15 @@
+
+
+
diff --git a/packages/svelte/tests/runtime-runes/samples/random/_config.js b/packages/svelte/tests/runtime-runes/samples/random/_config.js
new file mode 100644
index 000000000000..368dd20c6c8f
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/random/_config.js
@@ -0,0 +1,8 @@
+import { test } from '../../test';
+
+export default test({
+ test({ assert, target }) {
+ const [p1, p2] = target.querySelectorAll('p');
+ assert.notEqual(p1.textContent, p2.textContent);
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/random/main.svelte b/packages/svelte/tests/runtime-runes/samples/random/main.svelte
new file mode 100644
index 000000000000..e1ec0b564903
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/random/main.svelte
@@ -0,0 +1,6 @@
+
+
+{(Math.random() * m).toFixed(10)}
+{(Math.random() * m).toFixed(10)}
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 000000000000..de59e8c7c7c8
--- /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: `0 `,
+
+ test({ assert, target }) {
+ const btn = target.querySelector('button');
+
+ flushSync(() => btn?.click());
+ assert.htmlEqual(target.innerHTML, `1 `);
+ }
+});
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 000000000000..aee0b15e1beb
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/reactive-identifier/main.svelte
@@ -0,0 +1,13 @@
+
+
+ count += 1}>
+ {object}
+
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 000000000000..1f04f176af7e
--- /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 `` element should be an array, but it received a non-array value. The selection will be kept as is.'
+ ]
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/select-multiple-invalid-value/main.svelte b/packages/svelte/tests/runtime-runes/samples/select-multiple-invalid-value/main.svelte
new file mode 100644
index 000000000000..f3dfedf0e95a
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/select-multiple-invalid-value/main.svelte
@@ -0,0 +1,9 @@
+
+ option
+
+
+ option
+
+
+ option
+
diff --git a/packages/svelte/tests/runtime-runes/samples/side-effect-derived-array/_config.js b/packages/svelte/tests/runtime-runes/samples/side-effect-derived-array/_config.js
new file mode 100644
index 000000000000..5ad6f57e311f
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/side-effect-derived-array/_config.js
@@ -0,0 +1,23 @@
+import { flushSync } from 'svelte';
+import { test } from '../../test';
+
+export default test({
+ compileOptions: {
+ dev: true,
+ runes: true
+ },
+
+ test({ assert, target }) {
+ const [button1, button2] = target.querySelectorAll('button');
+
+ assert.throws(() => {
+ button1?.click();
+ flushSync();
+ }, /state_unsafe_mutation/);
+
+ assert.doesNotThrow(() => {
+ button2?.click();
+ flushSync();
+ });
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/side-effect-derived-array/main.svelte b/packages/svelte/tests/runtime-runes/samples/side-effect-derived-array/main.svelte
new file mode 100644
index 000000000000..6468dbebc9b7
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/side-effect-derived-array/main.svelte
@@ -0,0 +1,25 @@
+
+
+ (visibleExternal = true)}>external
+{#if visibleExternal}
+ {throws}
+{/if}
+ (visibleInternal = true)}>internal
+{#if visibleInternal}
+ {works}
+{/if}
+
diff --git a/packages/svelte/tests/runtime-runes/samples/side-effect-derived-date/_config.js b/packages/svelte/tests/runtime-runes/samples/side-effect-derived-date/_config.js
new file mode 100644
index 000000000000..5ad6f57e311f
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/side-effect-derived-date/_config.js
@@ -0,0 +1,23 @@
+import { flushSync } from 'svelte';
+import { test } from '../../test';
+
+export default test({
+ compileOptions: {
+ dev: true,
+ runes: true
+ },
+
+ test({ assert, target }) {
+ const [button1, button2] = target.querySelectorAll('button');
+
+ assert.throws(() => {
+ button1?.click();
+ flushSync();
+ }, /state_unsafe_mutation/);
+
+ assert.doesNotThrow(() => {
+ button2?.click();
+ flushSync();
+ });
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/side-effect-derived-date/main.svelte b/packages/svelte/tests/runtime-runes/samples/side-effect-derived-date/main.svelte
new file mode 100644
index 000000000000..a3c6aa629fec
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/side-effect-derived-date/main.svelte
@@ -0,0 +1,27 @@
+
+
+ (visibleExternal = true)}>external
+{#if visibleExternal}
+ {throws}
+{/if}
+ (visibleInternal = true)}>internal
+{#if visibleInternal}
+ {works}
+{/if}
+
diff --git a/packages/svelte/tests/runtime-runes/samples/side-effect-derived-map/_config.js b/packages/svelte/tests/runtime-runes/samples/side-effect-derived-map/_config.js
new file mode 100644
index 000000000000..5ad6f57e311f
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/side-effect-derived-map/_config.js
@@ -0,0 +1,23 @@
+import { flushSync } from 'svelte';
+import { test } from '../../test';
+
+export default test({
+ compileOptions: {
+ dev: true,
+ runes: true
+ },
+
+ test({ assert, target }) {
+ const [button1, button2] = target.querySelectorAll('button');
+
+ assert.throws(() => {
+ button1?.click();
+ flushSync();
+ }, /state_unsafe_mutation/);
+
+ assert.doesNotThrow(() => {
+ button2?.click();
+ flushSync();
+ });
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/side-effect-derived-map/main.svelte b/packages/svelte/tests/runtime-runes/samples/side-effect-derived-map/main.svelte
new file mode 100644
index 000000000000..bdd5ccb75c91
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/side-effect-derived-map/main.svelte
@@ -0,0 +1,27 @@
+
+
+ (visibleExternal = true)}>external
+{#if visibleExternal}
+ {throws}
+{/if}
+ (visibleInternal = true)}>internal
+{#if visibleInternal}
+ {works}
+{/if}
+
diff --git a/packages/svelte/tests/runtime-runes/samples/side-effect-derived-object/_config.js b/packages/svelte/tests/runtime-runes/samples/side-effect-derived-object/_config.js
new file mode 100644
index 000000000000..5ad6f57e311f
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/side-effect-derived-object/_config.js
@@ -0,0 +1,23 @@
+import { flushSync } from 'svelte';
+import { test } from '../../test';
+
+export default test({
+ compileOptions: {
+ dev: true,
+ runes: true
+ },
+
+ test({ assert, target }) {
+ const [button1, button2] = target.querySelectorAll('button');
+
+ assert.throws(() => {
+ button1?.click();
+ flushSync();
+ }, /state_unsafe_mutation/);
+
+ assert.doesNotThrow(() => {
+ button2?.click();
+ flushSync();
+ });
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/side-effect-derived-object/main.svelte b/packages/svelte/tests/runtime-runes/samples/side-effect-derived-object/main.svelte
new file mode 100644
index 000000000000..2174bde59f60
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/side-effect-derived-object/main.svelte
@@ -0,0 +1,25 @@
+
+
+ (visibleExternal = true)}>external
+{#if visibleExternal}
+ {throws}
+{/if}
+ (visibleInternal = true)}>internal
+{#if visibleInternal}
+ {works}
+{/if}
+
diff --git a/packages/svelte/tests/runtime-runes/samples/side-effect-derived-primitive/_config.js b/packages/svelte/tests/runtime-runes/samples/side-effect-derived-primitive/_config.js
new file mode 100644
index 000000000000..5ad6f57e311f
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/side-effect-derived-primitive/_config.js
@@ -0,0 +1,23 @@
+import { flushSync } from 'svelte';
+import { test } from '../../test';
+
+export default test({
+ compileOptions: {
+ dev: true,
+ runes: true
+ },
+
+ test({ assert, target }) {
+ const [button1, button2] = target.querySelectorAll('button');
+
+ assert.throws(() => {
+ button1?.click();
+ flushSync();
+ }, /state_unsafe_mutation/);
+
+ assert.doesNotThrow(() => {
+ button2?.click();
+ flushSync();
+ });
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/side-effect-derived-primitive/main.svelte b/packages/svelte/tests/runtime-runes/samples/side-effect-derived-primitive/main.svelte
new file mode 100644
index 000000000000..21de8b9d91c9
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/side-effect-derived-primitive/main.svelte
@@ -0,0 +1,25 @@
+
+
+ (visibleExternal = true)}>external
+{#if visibleExternal}
+ {throws}
+{/if}
+ (visibleInternal = true)}>internal
+{#if visibleInternal}
+ {works}
+{/if}
+
diff --git a/packages/svelte/tests/runtime-runes/samples/side-effect-derived-set/_config.js b/packages/svelte/tests/runtime-runes/samples/side-effect-derived-set/_config.js
new file mode 100644
index 000000000000..5ad6f57e311f
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/side-effect-derived-set/_config.js
@@ -0,0 +1,23 @@
+import { flushSync } from 'svelte';
+import { test } from '../../test';
+
+export default test({
+ compileOptions: {
+ dev: true,
+ runes: true
+ },
+
+ test({ assert, target }) {
+ const [button1, button2] = target.querySelectorAll('button');
+
+ assert.throws(() => {
+ button1?.click();
+ flushSync();
+ }, /state_unsafe_mutation/);
+
+ assert.doesNotThrow(() => {
+ button2?.click();
+ flushSync();
+ });
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/side-effect-derived-set/main.svelte b/packages/svelte/tests/runtime-runes/samples/side-effect-derived-set/main.svelte
new file mode 100644
index 000000000000..8564f6e7c48e
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/side-effect-derived-set/main.svelte
@@ -0,0 +1,27 @@
+
+
+ (visibleExternal = true)}>external
+{#if visibleExternal}
+ {throws}
+{/if}
+ (visibleInternal = true)}>internal
+{#if visibleInternal}
+ {works}
+{/if}
+
diff --git a/packages/svelte/tests/runtime-runes/samples/side-effect-derived-url-search-params/_config.js b/packages/svelte/tests/runtime-runes/samples/side-effect-derived-url-search-params/_config.js
new file mode 100644
index 000000000000..5ad6f57e311f
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/side-effect-derived-url-search-params/_config.js
@@ -0,0 +1,23 @@
+import { flushSync } from 'svelte';
+import { test } from '../../test';
+
+export default test({
+ compileOptions: {
+ dev: true,
+ runes: true
+ },
+
+ test({ assert, target }) {
+ const [button1, button2] = target.querySelectorAll('button');
+
+ assert.throws(() => {
+ button1?.click();
+ flushSync();
+ }, /state_unsafe_mutation/);
+
+ assert.doesNotThrow(() => {
+ button2?.click();
+ flushSync();
+ });
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/side-effect-derived-url-search-params/main.svelte b/packages/svelte/tests/runtime-runes/samples/side-effect-derived-url-search-params/main.svelte
new file mode 100644
index 000000000000..014a1e4e6dcf
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/side-effect-derived-url-search-params/main.svelte
@@ -0,0 +1,27 @@
+
+
+ (visibleExternal = true)}>external
+{#if visibleExternal}
+ {throws}
+{/if}
+ (visibleInternal = true)}>internal
+{#if visibleInternal}
+ {works}
+{/if}
+
diff --git a/packages/svelte/tests/runtime-runes/samples/side-effect-derived-url/_config.js b/packages/svelte/tests/runtime-runes/samples/side-effect-derived-url/_config.js
new file mode 100644
index 000000000000..5ad6f57e311f
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/side-effect-derived-url/_config.js
@@ -0,0 +1,23 @@
+import { flushSync } from 'svelte';
+import { test } from '../../test';
+
+export default test({
+ compileOptions: {
+ dev: true,
+ runes: true
+ },
+
+ test({ assert, target }) {
+ const [button1, button2] = target.querySelectorAll('button');
+
+ assert.throws(() => {
+ button1?.click();
+ flushSync();
+ }, /state_unsafe_mutation/);
+
+ assert.doesNotThrow(() => {
+ button2?.click();
+ flushSync();
+ });
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/side-effect-derived-url/main.svelte b/packages/svelte/tests/runtime-runes/samples/side-effect-derived-url/main.svelte
new file mode 100644
index 000000000000..69ead384c311
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/side-effect-derived-url/main.svelte
@@ -0,0 +1,27 @@
+
+
+ (visibleExternal = true)}>external
+{#if visibleExternal}
+ {throws}
+{/if}
+ (visibleInternal = true)}>internal
+{#if visibleInternal}
+ {works}
+{/if}
+
diff --git a/packages/svelte/tests/runtime-runes/samples/snippet-access-in-script/_config.js b/packages/svelte/tests/runtime-runes/samples/snippet-access-in-script/_config.js
new file mode 100644
index 000000000000..ed0ead960bdb
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/snippet-access-in-script/_config.js
@@ -0,0 +1,7 @@
+import { test } from '../../test';
+
+export default test({
+ compileOptions: {
+ dev: true
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/snippet-access-in-script/fn.js b/packages/svelte/tests/runtime-runes/samples/snippet-access-in-script/fn.js
new file mode 100644
index 000000000000..9e9a48c60cbe
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/snippet-access-in-script/fn.js
@@ -0,0 +1,3 @@
+export function fn(snippet) {
+ return snippet;
+}
diff --git a/packages/svelte/tests/runtime-runes/samples/snippet-access-in-script/main.svelte b/packages/svelte/tests/runtime-runes/samples/snippet-access-in-script/main.svelte
new file mode 100644
index 000000000000..cc73aa31db18
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/snippet-access-in-script/main.svelte
@@ -0,0 +1,10 @@
+
+
+{#snippet test()}
+ {variable}
+{/snippet}
\ No newline at end of file
diff --git a/packages/svelte/tests/runtime-runes/samples/snippet-block-without-render-tag-dev/_config.js b/packages/svelte/tests/runtime-runes/samples/snippet-block-without-render-tag-dev/_config.js
new file mode 100644
index 000000000000..94c5de10af60
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/snippet-block-without-render-tag-dev/_config.js
@@ -0,0 +1,8 @@
+import { test } from '../../test';
+
+export default test({
+ compileOptions: {
+ dev: true
+ },
+ runtime_error: 'snippet_without_render_tag'
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/snippet-block-without-render-tag-dev/main.svelte b/packages/svelte/tests/runtime-runes/samples/snippet-block-without-render-tag-dev/main.svelte
new file mode 100644
index 000000000000..3f8edfe4fafc
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/snippet-block-without-render-tag-dev/main.svelte
@@ -0,0 +1,5 @@
+{testSnippet}
+
+{#snippet testSnippet()}
+ hi again
+{/snippet}
diff --git a/packages/svelte/tests/runtime-runes/samples/snippet-block-without-render-tag-prod/_config.js b/packages/svelte/tests/runtime-runes/samples/snippet-block-without-render-tag-prod/_config.js
new file mode 100644
index 000000000000..f47bee71df87
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/snippet-block-without-render-tag-prod/_config.js
@@ -0,0 +1,3 @@
+import { test } from '../../test';
+
+export default test({});
diff --git a/packages/svelte/tests/runtime-runes/samples/snippet-block-without-render-tag-prod/main.svelte b/packages/svelte/tests/runtime-runes/samples/snippet-block-without-render-tag-prod/main.svelte
new file mode 100644
index 000000000000..3f8edfe4fafc
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/snippet-block-without-render-tag-prod/main.svelte
@@ -0,0 +1,5 @@
+{testSnippet}
+
+{#snippet testSnippet()}
+ hi again
+{/snippet}
diff --git a/packages/svelte/tests/runtime-runes/samples/snippet-children-without-render-tag-dev-prod/_config.js b/packages/svelte/tests/runtime-runes/samples/snippet-children-without-render-tag-dev-prod/_config.js
new file mode 100644
index 000000000000..f47bee71df87
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/snippet-children-without-render-tag-dev-prod/_config.js
@@ -0,0 +1,3 @@
+import { test } from '../../test';
+
+export default test({});
diff --git a/packages/svelte/tests/runtime-runes/samples/snippet-children-without-render-tag-dev-prod/main.svelte b/packages/svelte/tests/runtime-runes/samples/snippet-children-without-render-tag-dev-prod/main.svelte
new file mode 100644
index 000000000000..4a4ed3176f24
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/snippet-children-without-render-tag-dev-prod/main.svelte
@@ -0,0 +1,5 @@
+
+
+Hi
diff --git a/packages/svelte/tests/runtime-runes/samples/snippet-children-without-render-tag-dev-prod/unrendered-children.svelte b/packages/svelte/tests/runtime-runes/samples/snippet-children-without-render-tag-dev-prod/unrendered-children.svelte
new file mode 100644
index 000000000000..6b7154a5a406
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/snippet-children-without-render-tag-dev-prod/unrendered-children.svelte
@@ -0,0 +1,5 @@
+
+
+{children}
diff --git a/packages/svelte/tests/runtime-runes/samples/snippet-children-without-render-tag-dev/_config.js b/packages/svelte/tests/runtime-runes/samples/snippet-children-without-render-tag-dev/_config.js
new file mode 100644
index 000000000000..94c5de10af60
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/snippet-children-without-render-tag-dev/_config.js
@@ -0,0 +1,8 @@
+import { test } from '../../test';
+
+export default test({
+ compileOptions: {
+ dev: true
+ },
+ runtime_error: 'snippet_without_render_tag'
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/snippet-children-without-render-tag-dev/main.svelte b/packages/svelte/tests/runtime-runes/samples/snippet-children-without-render-tag-dev/main.svelte
new file mode 100644
index 000000000000..4a4ed3176f24
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/snippet-children-without-render-tag-dev/main.svelte
@@ -0,0 +1,5 @@
+
+
+Hi
diff --git a/packages/svelte/tests/runtime-runes/samples/snippet-children-without-render-tag-dev/unrendered-children.svelte b/packages/svelte/tests/runtime-runes/samples/snippet-children-without-render-tag-dev/unrendered-children.svelte
new file mode 100644
index 000000000000..6b7154a5a406
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/snippet-children-without-render-tag-dev/unrendered-children.svelte
@@ -0,0 +1,5 @@
+
+
+{children}
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 000000000000..05e3e80b7985
--- /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 000000000000..37345c629e8e
--- /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/snippet-invalid-call/_config.js b/packages/svelte/tests/runtime-runes/samples/snippet-invalid-call/_config.js
new file mode 100644
index 000000000000..7e72fd751ab6
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/snippet-invalid-call/_config.js
@@ -0,0 +1,9 @@
+import { test } from '../../test';
+
+export default test({
+ compileOptions: {
+ dev: true
+ },
+
+ error: 'invalid_snippet_arguments'
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/snippet-invalid-call/main.svelte b/packages/svelte/tests/runtime-runes/samples/snippet-invalid-call/main.svelte
new file mode 100644
index 000000000000..3c47db3f96eb
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/snippet-invalid-call/main.svelte
@@ -0,0 +1,7 @@
+
+
+{#snippet test()}
+ hello
+{/snippet}
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 000000000000..71802ba6de9a
--- /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 000000000000..fb1f7ddf9614
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/spread-element-input-select/main.svelte
@@ -0,0 +1,10 @@
+
+
+ spread = { disabled: false }}>
+
+
+ Hello
+ World
+
diff --git a/packages/svelte/tests/runtime-runes/samples/style-directive-mutations/_config.js b/packages/svelte/tests/runtime-runes/samples/style-directive-mutations/_config.js
new file mode 100644
index 000000000000..bd76e4e6b929
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/style-directive-mutations/_config.js
@@ -0,0 +1,95 @@
+import { flushSync, tick } from 'svelte';
+import { test } from '../../test';
+
+// This test counts mutations on hydration
+// set_style() should not mutate style on hydration, except if mismatch
+export default test({
+ mode: ['server', 'hydrate'],
+
+ server_props: {
+ browser: false
+ },
+
+ props: {
+ browser: true
+ },
+
+ ssrHtml: `
+
+
+
+
+
+
+
+
+
+
+
+ `,
+
+ html: `
+
+
+
+
+
+
+
+
+
+
+
+ `,
+
+ async test({ target, assert, component, instance }) {
+ flushSync();
+ tick();
+ assert.deepEqual(instance.get_and_clear_mutations(), ['MAIN']);
+
+ let divs = target.querySelectorAll('div');
+
+ // Note : we cannot compare HTML because set_style() use dom.style.cssText
+ // which can alter the format of the attribute...
+
+ divs.forEach((d) => assert.equal(d.style.margin, ''));
+ divs.forEach((d) => assert.equal(d.style.color, 'red'));
+ divs.forEach((d) => assert.equal(d.style.fontSize, '18px'));
+
+ component.margin = '1px';
+ flushSync();
+ assert.deepEqual(
+ instance.get_and_clear_mutations(),
+ ['DIV', 'DIV', 'DIV', 'DIV', 'DIV', 'DIV', 'DIV', 'DIV', 'DIV'],
+ 'margin'
+ );
+ divs.forEach((d) => assert.equal(d.style.margin, '1px'));
+
+ component.color = 'yellow';
+ flushSync();
+ assert.deepEqual(
+ instance.get_and_clear_mutations(),
+ ['DIV', 'DIV', 'DIV', 'DIV', 'DIV', 'DIV', 'DIV', 'DIV', 'DIV'],
+ 'color'
+ );
+ divs.forEach((d) => assert.equal(d.style.color, 'yellow'));
+
+ component.fontSize = '10px';
+ flushSync();
+ assert.deepEqual(
+ instance.get_and_clear_mutations(),
+ ['DIV', 'DIV', 'DIV', 'DIV', 'DIV', 'DIV', 'DIV', 'DIV', 'DIV'],
+ 'fontSize'
+ );
+ divs.forEach((d) => assert.equal(d.style.fontSize, '10px'));
+
+ component.fontSize = null;
+ flushSync();
+ assert.deepEqual(
+ instance.get_and_clear_mutations(),
+ ['DIV', 'DIV', 'DIV', 'DIV', 'DIV', 'DIV', 'DIV', 'DIV', 'DIV'],
+ 'fontSize'
+ );
+ divs.forEach((d) => assert.equal(d.style.fontSize, ''));
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/style-directive-mutations/main.svelte b/packages/svelte/tests/runtime-runes/samples/style-directive-mutations/main.svelte
new file mode 100644
index 000000000000..ae4da8ae37c7
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/style-directive-mutations/main.svelte
@@ -0,0 +1,54 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/svelte/tests/runtime-runes/samples/style-update/_config.js b/packages/svelte/tests/runtime-runes/samples/style-update/_config.js
new file mode 100644
index 000000000000..52690a431a4d
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/style-update/_config.js
@@ -0,0 +1,65 @@
+import { flushSync } from 'svelte';
+import { test } from '../../test';
+
+const style_1 = 'invalid-key:0; margin:4px;;color: green ;color:blue ';
+const style_2 = ' other-key : 0 ; padding:2px; COLOR:green; color: blue';
+const style_2_normalized = 'padding: 2px; color: blue;';
+
+// https://github.com/sveltejs/svelte/issues/15309
+export default test({
+ props: {
+ style: style_1
+ },
+
+ ssrHtml: `
+
+
+
+
+
+ `,
+
+ async test({ assert, target, component }) {
+ component.style = style_2;
+ flushSync();
+
+ assert.htmlEqual(
+ target.innerHTML,
+ `
+
+
+
+
+
+ `
+ );
+
+ component.style = '';
+ flushSync();
+
+ assert.htmlEqual(
+ target.innerHTML,
+ `
+
+
+
+
+
+ `
+ );
+
+ component.style = null;
+ flushSync();
+
+ assert.htmlEqual(
+ target.innerHTML,
+ `
+
+
+
+
+
+ `
+ );
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/style-update/main.svelte b/packages/svelte/tests/runtime-runes/samples/style-update/main.svelte
new file mode 100644
index 000000000000..d29590d67035
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/style-update/main.svelte
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
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 000000000000..5668311b0e97
--- /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 000000000000..1d76b0dd0003
--- /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 000000000000..f49a48fb52f7
--- /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/svelte-meta-parent/_config.js b/packages/svelte/tests/runtime-runes/samples/svelte-meta-parent/_config.js
new file mode 100644
index 000000000000..eba85d5098ee
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/svelte-meta-parent/_config.js
@@ -0,0 +1,186 @@
+import { tick } from 'svelte';
+import { test } from '../../test';
+
+export default test({
+ mode: ['client', 'hydrate'],
+ compileOptions: {
+ dev: true
+ },
+ html: `
+ no parent
+ toggle
+ if
+ each
+ loading
+ key
+ hi
+ hi
+ hi
+ hi
+ hi
+ `,
+
+ async test({ target, assert }) {
+ function check() {
+ const [main, if_, each, await_, key, child1, child2, child3, child4, dynamic] =
+ target.querySelectorAll('p');
+
+ // @ts-expect-error
+ assert.deepEqual(main.__svelte_meta.parent, null);
+
+ // @ts-expect-error
+ assert.deepEqual(if_.__svelte_meta.parent, {
+ file: 'main.svelte',
+ type: 'if',
+ line: 12,
+ column: 0,
+ parent: null
+ });
+
+ // @ts-expect-error
+ assert.deepEqual(each.__svelte_meta.parent, {
+ file: 'main.svelte',
+ type: 'each',
+ line: 16,
+ column: 0,
+ parent: null
+ });
+
+ // @ts-expect-error
+ assert.deepEqual(await_.__svelte_meta.parent, {
+ file: 'main.svelte',
+ type: 'await',
+ line: 20,
+ column: 0,
+ parent: null
+ });
+
+ // @ts-expect-error
+ assert.deepEqual(key.__svelte_meta.parent, {
+ file: 'main.svelte',
+ type: 'key',
+ line: 26,
+ column: 0,
+ parent: null
+ });
+
+ // @ts-expect-error
+ assert.deepEqual(child1.__svelte_meta.parent, {
+ file: 'main.svelte',
+ type: 'component',
+ componentTag: 'Child',
+ line: 30,
+ column: 0,
+ parent: null
+ });
+
+ // @ts-expect-error
+ assert.deepEqual(child2.__svelte_meta.parent, {
+ file: 'main.svelte',
+ type: 'component',
+ componentTag: 'Child',
+ line: 33,
+ column: 1,
+ parent: {
+ file: 'passthrough.svelte',
+ type: 'render',
+ line: 5,
+ column: 0,
+ parent: {
+ file: 'main.svelte',
+ type: 'component',
+ componentTag: 'Passthrough',
+ line: 32,
+ column: 0,
+ parent: null
+ }
+ }
+ });
+
+ // @ts-expect-error
+ assert.deepEqual(child3.__svelte_meta.parent, {
+ file: 'main.svelte',
+ type: 'component',
+ componentTag: 'Child',
+ line: 38,
+ column: 2,
+ parent: {
+ file: 'passthrough.svelte',
+ type: 'render',
+ line: 5,
+ column: 0,
+ parent: {
+ file: 'main.svelte',
+ type: 'component',
+ componentTag: 'Passthrough',
+ line: 37,
+ column: 1,
+ parent: {
+ file: 'passthrough.svelte',
+ type: 'render',
+ line: 5,
+ column: 0,
+ parent: {
+ file: 'main.svelte',
+ type: 'component',
+ componentTag: 'Passthrough',
+ line: 36,
+ column: 0,
+ parent: null
+ }
+ }
+ }
+ }
+ });
+
+ // @ts-expect-error
+ assert.deepEqual(child4.__svelte_meta.parent, {
+ file: 'passthrough.svelte',
+ type: 'render',
+ line: 8,
+ column: 1,
+ parent: {
+ file: 'passthrough.svelte',
+ type: 'if',
+ line: 7,
+ column: 0,
+ parent: {
+ file: 'main.svelte',
+ type: 'component',
+ componentTag: 'Passthrough',
+ line: 43,
+ column: 1,
+ parent: {
+ file: 'main.svelte',
+ type: 'if',
+ line: 42,
+ column: 0,
+ parent: null
+ }
+ }
+ }
+ });
+
+ // @ts-expect-error
+ assert.deepEqual(dynamic.__svelte_meta.parent, {
+ file: 'main.svelte',
+ type: 'component',
+ componentTag: 'x.y',
+ line: 50,
+ column: 0,
+ parent: null
+ });
+ }
+
+ await tick();
+ check();
+
+ // Test that stack is kept when re-rendering
+ const button = target.querySelector('button');
+ button?.click();
+ await tick();
+ button?.click();
+ await tick();
+ check();
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/svelte-meta-parent/child.svelte b/packages/svelte/tests/runtime-runes/samples/svelte-meta-parent/child.svelte
new file mode 100644
index 000000000000..0df6def59374
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/svelte-meta-parent/child.svelte
@@ -0,0 +1 @@
+hi
\ No newline at end of file
diff --git a/packages/svelte/tests/runtime-runes/samples/svelte-meta-parent/main.svelte b/packages/svelte/tests/runtime-runes/samples/svelte-meta-parent/main.svelte
new file mode 100644
index 000000000000..b9bd46d8f781
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/svelte-meta-parent/main.svelte
@@ -0,0 +1,50 @@
+
+
+no parent
+ show = !show}>toggle
+
+{#if true}
+ if
+{/if}
+
+{#each [1]}
+ each
+{/each}
+
+{#await Promise.resolve()}
+ loading
+{:then}
+ await
+{/await}
+
+{#key key}
+ key
+{/key}
+
+
+
+
+
+
+
+
+
+
+
+
+
+{#if show}
+
+ {#snippet named()}
+ hi
+ {/snippet}
+
+{/if}
+
+
diff --git a/packages/svelte/tests/runtime-runes/samples/svelte-meta-parent/passthrough.svelte b/packages/svelte/tests/runtime-runes/samples/svelte-meta-parent/passthrough.svelte
new file mode 100644
index 000000000000..70ba81a4c131
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/svelte-meta-parent/passthrough.svelte
@@ -0,0 +1,9 @@
+
+
+{@render children?.()}
+
+{#if true}
+ {@render named?.()}
+{/if}
diff --git a/packages/svelte/tests/runtime-runes/samples/toStore-subscribe2/_config.js b/packages/svelte/tests/runtime-runes/samples/toStore-subscribe2/_config.js
new file mode 100644
index 000000000000..bc1793e7a461
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/toStore-subscribe2/_config.js
@@ -0,0 +1,16 @@
+import { flushSync } from 'svelte';
+import { test } from '../../test';
+
+export default test({
+ async test({ assert, target }) {
+ let btn = target.querySelector('button');
+
+ btn?.click();
+ flushSync();
+
+ assert.htmlEqual(
+ target.innerHTML,
+ `Count 1!
Count from store 1!
Add 1 `
+ );
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/toStore-subscribe2/main.svelte b/packages/svelte/tests/runtime-runes/samples/toStore-subscribe2/main.svelte
new file mode 100644
index 000000000000..82d20105b8eb
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/toStore-subscribe2/main.svelte
@@ -0,0 +1,11 @@
+
+
+Count {counter}!
+Count from store {$count}!
+
+ counter+=1}>Add 1
diff --git a/packages/svelte/tests/runtime-runes/samples/toStore-teardown/_config.js b/packages/svelte/tests/runtime-runes/samples/toStore-teardown/_config.js
new file mode 100644
index 000000000000..95904f011fc0
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/toStore-teardown/_config.js
@@ -0,0 +1,13 @@
+import { flushSync } from 'svelte';
+import { test } from '../../test';
+
+export default test({
+ async test({ assert, target }) {
+ let [, btn2] = target.querySelectorAll('button');
+
+ btn2.click();
+ flushSync();
+
+ assert.htmlEqual(target.innerHTML, `Set data Clear data `);
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/toStore-teardown/child.svelte b/packages/svelte/tests/runtime-runes/samples/toStore-teardown/child.svelte
new file mode 100644
index 000000000000..f1b1b7b49769
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/toStore-teardown/child.svelte
@@ -0,0 +1,11 @@
+
+
+
+ Current value:
+ {$currentValue}
+
diff --git a/packages/svelte/tests/runtime-runes/samples/toStore-teardown/main.svelte b/packages/svelte/tests/runtime-runes/samples/toStore-teardown/main.svelte
new file mode 100644
index 000000000000..7d36dd95cbfd
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/toStore-teardown/main.svelte
@@ -0,0 +1,15 @@
+
+
+Set data
+Clear data
+
+{#if data}
+
+{/if}
diff --git a/packages/svelte/tests/runtime-runes/samples/typescript/main.svelte b/packages/svelte/tests/runtime-runes/samples/typescript/main.svelte
index e2942b21f386..4fc7c4ec38d9 100644
--- a/packages/svelte/tests/runtime-runes/samples/typescript/main.svelte
+++ b/packages/svelte/tests/runtime-runes/samples/typescript/main.svelte
@@ -8,12 +8,13 @@
console.log(this);
}
- function foo(): string {
+ function foo(): string {
return ""!;
}
class Foo {
public name: string;
+ declare bar: string;
x = 'x' as const;
constructor(name: string) {
this.name = name;
@@ -45,6 +46,8 @@
export type { Hello };
const TypedFoo = Foo;
+ const typeAssertion = true;
+ const x = foo();
+
+
+
+{#snippet snip()}
+ snippet {x}
+{/snippet}
diff --git a/packages/svelte/tests/runtime-runes/samples/untrack-own-deriveds/_config.js b/packages/svelte/tests/runtime-runes/samples/untrack-own-deriveds/_config.js
new file mode 100644
index 000000000000..18062b86fb43
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/untrack-own-deriveds/_config.js
@@ -0,0 +1,20 @@
+import { flushSync } from 'svelte';
+import { test } from '../../test';
+
+export default test({
+ test({ assert, target, logs }) {
+ const button = target.querySelector('button');
+
+ flushSync(() => button?.click());
+
+ assert.htmlEqual(
+ target.innerHTML,
+ `
+ increment
+ 1/2
+ class Foo {
+ value = $state(0);
+ double = $derived(this.value * 2);
+
+ constructor() {
+ console.log(this.value, this.double);
+ }
+
+ increment() {
+ this.value++;
+ }
+ }
+
+ let foo = $state();
+
+ $effect(() => {
+ foo = new Foo();
+ });
+
+
+ foo.increment()}>increment
+
+{#if foo}
+ {foo.value}/{foo.double}
+{/if}
diff --git a/packages/svelte/tests/runtime-runes/samples/untracked-write-pre/_config.js b/packages/svelte/tests/runtime-runes/samples/untracked-write-pre/_config.js
new file mode 100644
index 000000000000..0310ec4fbb52
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/untracked-write-pre/_config.js
@@ -0,0 +1,7 @@
+import { test } from '../../test';
+
+export default test({
+ test({ assert, target, logs }) {
+ assert.deepEqual(logs, ['Outer', 'Inner', 'Outer', 'Inner']);
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/untracked-write-pre/main.svelte b/packages/svelte/tests/runtime-runes/samples/untracked-write-pre/main.svelte
new file mode 100644
index 000000000000..5e95dbfd411a
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/untracked-write-pre/main.svelte
@@ -0,0 +1,13 @@
+
diff --git a/packages/svelte/tests/runtime-runes/samples/validate-undefined-snippet-default-arg/_config.js b/packages/svelte/tests/runtime-runes/samples/validate-undefined-snippet-default-arg/_config.js
new file mode 100644
index 000000000000..bddb75e67785
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/validate-undefined-snippet-default-arg/_config.js
@@ -0,0 +1,5 @@
+import { test } from '../../test';
+
+export default test({
+ html: `default
`
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/validate-undefined-snippet-default-arg/main.svelte b/packages/svelte/tests/runtime-runes/samples/validate-undefined-snippet-default-arg/main.svelte
new file mode 100644
index 000000000000..3f00eba46bc2
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/validate-undefined-snippet-default-arg/main.svelte
@@ -0,0 +1,5 @@
+{#snippet test(param = "default")}
+ {param}
+{/snippet}
+
+{@render test()}
\ No newline at end of file
diff --git a/packages/svelte/tests/runtime-runes/samples/writable-derived-2/_config.js b/packages/svelte/tests/runtime-runes/samples/writable-derived-2/_config.js
new file mode 100644
index 000000000000..fde3e7b1eada
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/writable-derived-2/_config.js
@@ -0,0 +1,7 @@
+import { test } from '../../test';
+
+export default test({
+ html: `true true`,
+
+ test({ assert, target, window }) {}
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/writable-derived-2/main.svelte b/packages/svelte/tests/runtime-runes/samples/writable-derived-2/main.svelte
new file mode 100644
index 000000000000..741aa6912542
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/writable-derived-2/main.svelte
@@ -0,0 +1,9 @@
+
+
+{expect1} {expect2}
diff --git a/packages/svelte/tests/runtime-runes/samples/writable-derived-2/util.svelte.js b/packages/svelte/tests/runtime-runes/samples/writable-derived-2/util.svelte.js
new file mode 100644
index 000000000000..8e862753ab90
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/writable-derived-2/util.svelte.js
@@ -0,0 +1,17 @@
+export const createAppState = (options) => {
+ const source = $derived(options.source());
+ let value = $derived(source);
+
+ return {
+ get value() {
+ return value;
+ },
+ onChange(nextValue) {
+ value = nextValue;
+ }
+ };
+};
+
+const result = createAppState({ source: () => 'wrong' });
+result.onChange('right');
+export const expect2 = result.value === 'right';
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 000000000000..999e4ad6e05c
--- /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 000000000000..0b20f811c338
--- /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/runtime-runes/samples/writable-derived/_config.js b/packages/svelte/tests/runtime-runes/samples/writable-derived/_config.js
new file mode 100644
index 000000000000..b48ccbdfd004
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/writable-derived/_config.js
@@ -0,0 +1,46 @@
+import { flushSync } from 'svelte';
+import { test } from '../../test';
+
+export default test({
+ html: `
+ 0 * 2 = 0
+ `,
+
+ ssrHtml: `
+ 0 * 2 = 0
+ `,
+
+ test({ assert, target, window }) {
+ const [input1, input2] = target.querySelectorAll('input');
+
+ flushSync(() => {
+ input1.value = '10';
+ input1.dispatchEvent(new window.Event('input', { bubbles: true }));
+ });
+
+ assert.htmlEqual(
+ target.innerHTML,
+ `10 * 2 = 20
`
+ );
+
+ flushSync(() => {
+ input2.value = '99';
+ input2.dispatchEvent(new window.Event('input', { bubbles: true }));
+ });
+
+ assert.htmlEqual(
+ target.innerHTML,
+ `10 * 2 = 99
`
+ );
+
+ flushSync(() => {
+ input1.value = '20';
+ input1.dispatchEvent(new window.Event('input', { bubbles: true }));
+ });
+
+ assert.htmlEqual(
+ target.innerHTML,
+ `20 * 2 = 40
`
+ );
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/writable-derived/main.svelte b/packages/svelte/tests/runtime-runes/samples/writable-derived/main.svelte
new file mode 100644
index 000000000000..ab1dde0b9bba
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/writable-derived/main.svelte
@@ -0,0 +1,9 @@
+
+
+
+
+
+{count} * 2 = {double}
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 000000000000..e3f755a08c1d
--- /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 000000000000..9414735f2fad
--- /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 000000000000..213a5f5bf155
--- /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 000000000000..422548cd14fe
--- /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 000000000000..96d1d8b2333f
--- /dev/null
+++ b/packages/svelte/tests/server-side-rendering/samples/select-value-bind-store/_expected.html
@@ -0,0 +1 @@
+--Please choose an option-- Dog Cat
\ 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 000000000000..cb3b554762dd
--- /dev/null
+++ b/packages/svelte/tests/server-side-rendering/samples/select-value-bind-store/main.svelte
@@ -0,0 +1,10 @@
+
+
+
+ --Please choose an option--
+ Dog
+ Cat
+
\ 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 000000000000..a17413bd8d05
--- /dev/null
+++ b/packages/svelte/tests/server-side-rendering/samples/select-value-component/Option.svelte
@@ -0,0 +1,5 @@
+
+
+{@render props.children?.()}
\ 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 000000000000..96d1d8b2333f
--- /dev/null
+++ b/packages/svelte/tests/server-side-rendering/samples/select-value-component/_expected.html
@@ -0,0 +1 @@
+--Please choose an option-- Dog Cat
\ 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 000000000000..e545833759b0
--- /dev/null
+++ b/packages/svelte/tests/server-side-rendering/samples/select-value-component/main.svelte
@@ -0,0 +1,8 @@
+
+
+ --Please choose an option--
+ Dog
+ Cat
+
\ 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 000000000000..de7cba88372e
--- /dev/null
+++ b/packages/svelte/tests/server-side-rendering/samples/select-value-implicit-value-complex/_expected.html
@@ -0,0 +1 @@
+--Please choose an option-- dog cat
\ 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 000000000000..45ae9aefe0a9
--- /dev/null
+++ b/packages/svelte/tests/server-side-rendering/samples/select-value-implicit-value-complex/main.svelte
@@ -0,0 +1,13 @@
+
+
+ {@render option("--Please choose an option--")}
+
+
+ {@render option("dog")}
+
+
+ {@render option("cat")}
+
+
+
+{#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 000000000000..de7cba88372e
--- /dev/null
+++ b/packages/svelte/tests/server-side-rendering/samples/select-value-implicit-value/_expected.html
@@ -0,0 +1 @@
+--Please choose an option-- dog cat
\ 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 000000000000..2308a4e7a989
--- /dev/null
+++ b/packages/svelte/tests/server-side-rendering/samples/select-value-implicit-value/main.svelte
@@ -0,0 +1,5 @@
+
+ --Please choose an option--
+ dog
+ cat
+
\ 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 000000000000..96d1d8b2333f
--- /dev/null
+++ b/packages/svelte/tests/server-side-rendering/samples/select-value/_expected.html
@@ -0,0 +1 @@
+--Please choose an option-- Dog Cat
\ 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 000000000000..811d752d4e40
--- /dev/null
+++ b/packages/svelte/tests/server-side-rendering/samples/select-value/main.svelte
@@ -0,0 +1,5 @@
+
+ --Please choose an option--
+ Dog
+ Cat
+
\ No newline at end of file
diff --git a/packages/svelte/tests/signals/test.ts b/packages/svelte/tests/signals/test.ts
index ef4cf16d3b6c..719c936df002 100644
--- a/packages/svelte/tests/signals/test.ts
+++ b/packages/svelte/tests/signals/test.ts
@@ -15,6 +15,7 @@ import { derived } from '../../src/internal/client/reactivity/deriveds';
import { snapshot } from '../../src/internal/shared/clone.js';
import { SvelteSet } from '../../src/reactivity/set';
import { DESTROYED } from '../../src/internal/client/constants';
+import { noop } from 'svelte/internal/client';
/**
* @param runes runes mode
@@ -111,6 +112,45 @@ describe('signals', () => {
};
});
+ test('unowned deriveds are not added as reactions but trigger effects', () => {
+ var obj = state(undefined);
+
+ class C1 {
+ #v = state(0);
+ get v() {
+ return $.get(this.#v);
+ }
+ set v(v: number) {
+ set(this.#v, v);
+ }
+ }
+
+ return () => {
+ let d = derived(() => $.get(obj)?.v || '-');
+
+ const log: number[] = [];
+ assert.equal($.get(d), '-');
+
+ let destroy = effect_root(() => {
+ render_effect(() => {
+ log.push($.get(d));
+ });
+ });
+
+ set(obj, new C1());
+ flushSync();
+ assert.equal($.get(d), '-');
+ $.get(obj).v = 1;
+ flushSync();
+ assert.equal($.get(d), 1);
+ assert.deepEqual(log, ['-', 1]);
+ destroy();
+ // ensure we're not leaking reactions
+ assert.equal(obj.reactions, null);
+ assert.equal(d.reactions, null);
+ };
+ });
+
test('derived from state', () => {
const log: number[] = [];
@@ -469,6 +509,9 @@ describe('signals', () => {
test('schedules rerun when writing to signal before reading it', (runes) => {
if (!runes) return () => {};
+ const error = console.error;
+ console.error = noop;
+
const value = state({ count: 0 });
user_effect(() => {
set(value, { count: 0 });
@@ -482,14 +525,44 @@ describe('signals', () => {
} catch (e: any) {
assert.include(e.message, 'effect_update_depth_exceeded');
errored = true;
+ } finally {
+ assert.equal(errored, true);
+ console.error = error;
+ }
+ };
+ });
+
+ test('schedules rerun when updating deeply nested value', (runes) => {
+ if (!runes) return () => {};
+
+ const error = console.error;
+ console.error = noop;
+
+ const value = proxy({ a: { b: { c: 0 } } });
+ user_effect(() => {
+ value.a.b.c += 1;
+ });
+
+ return () => {
+ let errored = false;
+ try {
+ flushSync();
+ } catch (e: any) {
+ assert.include(e.message, 'effect_update_depth_exceeded');
+ errored = true;
+ } finally {
+ assert.equal(errored, true);
+ console.error = error;
}
- assert.equal(errored, true);
};
});
test('schedules rerun when writing to signal before reading it', (runes) => {
if (!runes) return () => {};
+ const error = console.error;
+ console.error = noop;
+
const value = proxy({ arr: [] });
user_effect(() => {
value.arr = [];
@@ -503,8 +576,10 @@ describe('signals', () => {
} catch (e: any) {
assert.include(e.message, 'effect_update_depth_exceeded');
errored = true;
+ } finally {
+ assert.equal(errored, true);
+ console.error = error;
}
- assert.equal(errored, true);
};
});
@@ -958,14 +1033,65 @@ describe('signals', () => {
};
});
- test('deriveds cannot depend on state they own', () => {
+ test('deriveds do not depend on state they own', () => {
return () => {
+ let s;
+
const d = derived(() => {
- const s = state(0);
+ s = state(0);
return $.get(s);
});
- assert.throws(() => $.get(d), 'state_unsafe_local_read');
+ assert.equal($.get(d), 0);
+
+ set(s!, 1);
+ assert.equal($.get(d), 0);
+ };
+ });
+
+ test('effects do not depend on state they own', () => {
+ user_effect(() => {
+ const value = state(0);
+ set(value, $.get(value) + 1);
+ });
+
+ return () => {
+ flushSync();
+ };
+ });
+
+ test('nested effects depend on state of upper effects', () => {
+ const logs: number[] = [];
+
+ user_effect(() => {
+ const raw = state(0);
+ const proxied = proxy({ current: 0 });
+
+ // We need those separate, else one working and rerunning the effect
+ // could mask the other one not rerunning
+ user_effect(() => {
+ logs.push($.get(raw));
+ });
+
+ user_effect(() => {
+ logs.push(proxied.current);
+ });
+
+ // Important so that the updating effect is not running
+ // together with the reading effects
+ flushSync();
+
+ user_effect(() => {
+ $.untrack(() => {
+ set(raw, $.get(raw) + 1);
+ proxied.current += 1;
+ });
+ });
+ });
+
+ return () => {
+ flushSync();
+ assert.deepEqual(logs, [0, 0, 1, 1]);
};
});
@@ -1044,6 +1170,38 @@ describe('signals', () => {
};
});
+ test("deriveds set after they are DIRTY doesn't get updated on get", () => {
+ return () => {
+ const a = state(0);
+ const b = derived(() => $.get(a));
+
+ set(b, 1);
+ assert.equal($.get(b), 1);
+
+ set(a, 2);
+ assert.equal($.get(b), 2);
+ set(b, 3);
+
+ assert.equal($.get(b), 3);
+ };
+ });
+
+ test("unowned deriveds set after they are DIRTY doesn't get updated on get", () => {
+ return () => {
+ const a = state(0);
+ const b = derived(() => $.get(a));
+ const c = derived(() => $.get(b));
+
+ set(b, 1);
+ assert.equal($.get(c), 1);
+
+ set(a, 2);
+
+ assert.equal($.get(b), 2);
+ assert.equal($.get(c), 2);
+ };
+ });
+
test('deriveds containing effects work correctly when used with untrack', () => {
return () => {
let a = render_effect(() => {});
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 3e5a12ed9dec..9bb45ebf78e6 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/await-block-scope/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/await-block-scope/_expected/server/index.svelte.js
index 012789a5509b..4b6e32d58e0a 100644
--- a/packages/svelte/tests/snapshot/samples/await-block-scope/_expected/server/index.svelte.js
+++ b/packages/svelte/tests/snapshot/samples/await-block-scope/_expected/server/index.svelte.js
@@ -8,7 +8,7 @@ export default function Await_block_scope($$payload) {
counter.count += 1;
}
- $$payload.out += `clicks: ${$.escape(counter.count)} `;
- $.await(promise, () => {}, (counter) => {}, () => {});
- $$payload.out += ` ${$.escape(counter.count)}`;
+ $$payload.out += `clicks: ${$.escape(counter.count)} `;
+ $.await($$payload, promise, () => {}, (counter) => {});
+ $$payload.out += ` ${$.escape(counter.count)}`;
}
\ No newline at end of file
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 fa990b33ee56..a87a356d580b 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('');
@@ -22,8 +22,9 @@ export default function Bind_component_snippet($$anchor) {
get value() {
return $.get(value);
},
+
set value($$value) {
- $.set(value, $.proxy($$value));
+ $.set(value, $$value, true);
}
});
diff --git a/packages/svelte/tests/snapshot/samples/bind-component-snippet/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/bind-component-snippet/_expected/server/index.svelte.js
index c091179c41ae..e2c0ee29a587 100644
--- a/packages/svelte/tests/snapshot/samples/bind-component-snippet/_expected/server/index.svelte.js
+++ b/packages/svelte/tests/snapshot/samples/bind-component-snippet/_expected/server/index.svelte.js
@@ -16,6 +16,7 @@ export default function Bind_component_snippet($$payload) {
get value() {
return value;
},
+
set value($$value) {
value = $$value;
$$settled = false;
@@ -23,7 +24,7 @@ export default function Bind_component_snippet($$payload) {
});
$$payload.out += ` value: ${$.escape(value)}`;
- };
+ }
do {
$$settled = true;
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 2898f31a6fb5..c9725d6718dd 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,21 +5,42 @@ 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);
}
set a(value) {
- $.set(this.#a, $.proxy(value));
+ $.set(this.#a, value, true);
}
#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;
- this.#b.v = 2;
+ $.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 2a115a498373..abfc264fea0a 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 a3ff5917e77f..127cfd4d2a7d 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 9651713c52f5..e9cf5b573d6d 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, $.proxy(array[0])),
- $.set(b, $.proxy(array[1]))
- );
+ ((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 d97a58bf40d8..d84b674f88f4 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,18 +1,25 @@
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
let x = 'test';
+
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);
@@ -21,13 +28,11 @@ export default function Main($$anchor) {
$.template_effect(() => $.set_custom_element_data(custom_element_1, 'fooBar', y()));
$.template_effect(
- ($0) => {
- $.set_attribute(div, 'foobar', x);
- $.set_attribute(svg, 'viewBox', x);
+ ($0, $1) => {
$.set_attribute(div_1, 'foobar', $0);
- $.set_attribute(svg_1, 'viewBox', $0);
+ $.set_attribute(svg_1, 'viewBox', $1);
},
- [y]
+ [y, y]
);
$.append($$anchor, fragment);
diff --git a/packages/svelte/tests/snapshot/samples/dynamic-attributes-casing/_expected/server/main.svelte.js b/packages/svelte/tests/snapshot/samples/dynamic-attributes-casing/_expected/server/main.svelte.js
index 4ea5edb6a0ac..cf731d8187b4 100644
--- a/packages/svelte/tests/snapshot/samples/dynamic-attributes-casing/_expected/server/main.svelte.js
+++ b/packages/svelte/tests/snapshot/samples/dynamic-attributes-casing/_expected/server/main.svelte.js
@@ -3,6 +3,7 @@ import * as $ from 'svelte/internal/server';
export default function Main($$payload) {
// needs to be a snapshot test because jsdom does auto-correct the attribute casing
let x = 'test';
+
let y = () => 'test';
$$payload.out += `
`;
diff --git a/packages/svelte/tests/snapshot/samples/each-index-non-null/_config.js b/packages/svelte/tests/snapshot/samples/each-index-non-null/_config.js
new file mode 100644
index 000000000000..f47bee71df87
--- /dev/null
+++ b/packages/svelte/tests/snapshot/samples/each-index-non-null/_config.js
@@ -0,0 +1,3 @@
+import { test } from '../../test';
+
+export default test({});
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
new file mode 100644
index 000000000000..804a7c26f182
--- /dev/null
+++ b/packages/svelte/tests/snapshot/samples/each-index-non-null/_expected/client/index.svelte.js
@@ -0,0 +1,19 @@
+import 'svelte/internal/disclose-version';
+import 'svelte/internal/flags/legacy';
+import * as $ from 'svelte/internal/client';
+
+var root_1 = $.from_html(`
`);
+
+export default function Each_index_non_null($$anchor) {
+ var fragment = $.comment();
+ var node = $.first_child(fragment);
+
+ $.each(node, 0, () => Array(10), $.index, ($$anchor, $$item, i) => {
+ var p = root_1();
+
+ p.textContent = `index: ${i}`;
+ $.append($$anchor, p);
+ });
+
+ $.append($$anchor, fragment);
+}
\ No newline at end of file
diff --git a/packages/svelte/tests/snapshot/samples/each-index-non-null/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/each-index-non-null/_expected/server/index.svelte.js
new file mode 100644
index 000000000000..3431e36833b5
--- /dev/null
+++ b/packages/svelte/tests/snapshot/samples/each-index-non-null/_expected/server/index.svelte.js
@@ -0,0 +1,13 @@
+import * as $ from 'svelte/internal/server';
+
+export default function Each_index_non_null($$payload) {
+ const each_array = $.ensure_array_like(Array(10));
+
+ $$payload.out += ``;
+
+ for (let i = 0, $$length = each_array.length; i < $$length; i++) {
+ $$payload.out += `index: ${$.escape(i)}
`;
+ }
+
+ $$payload.out += ``;
+}
\ No newline at end of file
diff --git a/packages/svelte/tests/snapshot/samples/each-index-non-null/index.svelte b/packages/svelte/tests/snapshot/samples/each-index-non-null/index.svelte
new file mode 100644
index 000000000000..03bfc9e37299
--- /dev/null
+++ b/packages/svelte/tests/snapshot/samples/each-index-non-null/index.svelte
@@ -0,0 +1,3 @@
+{#each Array(10), i}
+ index: {i}
+{/each}
diff --git a/packages/svelte/tests/snapshot/samples/function-prop-no-getter/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/function-prop-no-getter/_expected/client/index.svelte.js
index c545608bcacf..218951b83610 100644
--- a/packages/svelte/tests/snapshot/samples/function-prop-no-getter/_expected/client/index.svelte.js
+++ b/packages/svelte/tests/snapshot/samples/function-prop-no-getter/_expected/client/index.svelte.js
@@ -13,7 +13,8 @@ export default function Function_prop_no_getter($$anchor) {
Button($$anchor, {
onmousedown: () => $.set(count, $.get(count) + 1),
onmouseup,
- onmouseenter: () => $.set(count, $.proxy(plusOne($.get(count)))),
+ onmouseenter: () => $.set(count, plusOne($.get(count)), true),
+
children: ($$anchor, $$slotProps) => {
$.next();
@@ -22,6 +23,7 @@ export default function Function_prop_no_getter($$anchor) {
$.template_effect(() => $.set_text(text, `clicks: ${$.get(count) ?? ''}`));
$.append($$anchor, text);
},
+
$$slots: { default: true }
});
}
\ No newline at end of file
diff --git a/packages/svelte/tests/snapshot/samples/function-prop-no-getter/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/function-prop-no-getter/_expected/server/index.svelte.js
index 88f6f55ee74a..7d37abd97b1c 100644
--- a/packages/svelte/tests/snapshot/samples/function-prop-no-getter/_expected/server/index.svelte.js
+++ b/packages/svelte/tests/snapshot/samples/function-prop-no-getter/_expected/server/index.svelte.js
@@ -13,9 +13,11 @@ export default function Function_prop_no_getter($$payload) {
onmousedown: () => count += 1,
onmouseup,
onmouseenter: () => count = plusOne(count),
+
children: ($$payload) => {
$$payload.out += `clicks: ${$.escape(count)}`;
},
+
$$slots: { default: true }
});
}
\ No newline at end of file
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 000000000000..23231c969b45
--- /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 000000000000..d4034dc55dd7
--- /dev/null
+++ b/packages/svelte/tests/snapshot/samples/functional-templating/_expected/client/index.svelte.js
@@ -0,0 +1,26 @@
+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 000000000000..dc49c0c213a2
--- /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 000000000000..c0fe8965b897
--- /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 899c126001f2..68fdaa4570cc 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 3c8322500b7c..1fac1338c5f9 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/imports-in-modules/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/imports-in-modules/_expected/client/index.svelte.js
index ebbe191dcbe4..0eab38919c5e 100644
--- a/packages/svelte/tests/snapshot/samples/imports-in-modules/_expected/client/index.svelte.js
+++ b/packages/svelte/tests/snapshot/samples/imports-in-modules/_expected/client/index.svelte.js
@@ -3,6 +3,4 @@ import 'svelte/internal/flags/legacy';
import * as $ from 'svelte/internal/client';
import { random } from './module.svelte';
-export default function Imports_in_modules($$anchor) {
-
-}
\ No newline at end of file
+export default function Imports_in_modules($$anchor) {}
\ No newline at end of file
diff --git a/packages/svelte/tests/snapshot/samples/imports-in-modules/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/imports-in-modules/_expected/server/index.svelte.js
index 4cd6bc59d782..2ed863d68f3a 100644
--- a/packages/svelte/tests/snapshot/samples/imports-in-modules/_expected/server/index.svelte.js
+++ b/packages/svelte/tests/snapshot/samples/imports-in-modules/_expected/server/index.svelte.js
@@ -1,6 +1,4 @@
import * as $ from 'svelte/internal/server';
import { random } from './module.svelte';
-export default function Imports_in_modules($$payload) {
-
-}
\ No newline at end of file
+export default function Imports_in_modules($$payload) {}
\ No newline at end of file
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 332c909ebed9..b46acee82e14 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';
@@ -10,11 +10,11 @@ export default function Nullish_coallescence_omittance($$anchor) {
var fragment = root();
var h1 = $.first_child(fragment);
- h1.textContent = `Hello, ${name ?? ''}!`;
+ h1.textContent = 'Hello, world!';
var b = $.sibling(h1, 2);
- b.textContent = `${1 ?? 'stuff'}${2 ?? 'more stuff'}${3 ?? 'even more stuff'}`;
+ b.textContent = '123';
var button = $.sibling(b, 2);
@@ -26,7 +26,7 @@ export default function Nullish_coallescence_omittance($$anchor) {
var h1_1 = $.sibling(button, 2);
- h1_1.textContent = `Hello, ${name ?? 'earth' ?? ''}`;
+ h1_1.textContent = 'Hello, world';
$.template_effect(() => $.set_text(text, `Count is ${$.get(count) ?? ''}`));
$.append($$anchor, fragment);
}
diff --git a/packages/svelte/tests/snapshot/samples/nullish-coallescence-omittance/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/nullish-coallescence-omittance/_expected/server/index.svelte.js
index 8181bfd98eeb..3b23befcd44e 100644
--- a/packages/svelte/tests/snapshot/samples/nullish-coallescence-omittance/_expected/server/index.svelte.js
+++ b/packages/svelte/tests/snapshot/samples/nullish-coallescence-omittance/_expected/server/index.svelte.js
@@ -4,5 +4,5 @@ export default function Nullish_coallescence_omittance($$payload) {
let name = 'world';
let count = 0;
- $$payload.out += `Hello, ${$.escape(name)}! ${$.escape(1 ?? 'stuff')}${$.escape(2 ?? 'more stuff')}${$.escape(3 ?? 'even more stuff')} Count is ${$.escape(count)} Hello, ${$.escape(name ?? 'earth' ?? null)} `;
+ $$payload.out += `Hello, world! 123 Count is ${$.escape(count)} Hello, world `;
}
\ No newline at end of file
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 940ed8f9e8fa..a351851875ed 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 = Math.max(0, Math.min(0, 100));
+ p.textContent = '0';
var p_1 = $.sibling(p, 2);
diff --git a/packages/svelte/tests/snapshot/samples/purity/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/purity/_expected/server/index.svelte.js
index 588332407a63..9457378c0db4 100644
--- a/packages/svelte/tests/snapshot/samples/purity/_expected/server/index.svelte.js
+++ b/packages/svelte/tests/snapshot/samples/purity/_expected/server/index.svelte.js
@@ -1,7 +1,7 @@
import * as $ from 'svelte/internal/server';
export default function Purity($$payload) {
- $$payload.out += `${$.escape(Math.max(0, Math.min(0, 100)))}
${$.escape(location.href)}
`;
+ $$payload.out += `0
${$.escape(location.href)}
`;
Child($$payload, { prop: encodeURIComponent('hello') });
$$payload.out += ``;
}
\ No newline at end of file
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 46d376aca2f9..78147659ff40 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
a `, 3);
+var root = $.from_html(` we don't need to traverse these nodes
or
these
ones
these
trailing
nodes
can
be
completely
ignored
a `, 3);
export default function Skip_static_subtree($$anchor, $$props) {
var fragment = root();
@@ -13,7 +13,7 @@ export default function Skip_static_subtree($$anchor, $$props) {
var node = $.sibling(h1, 10);
- $.html(node, () => $$props.content, false, false);
+ $.html(node, () => $$props.content);
$.next(14);
$.reset(main);
@@ -38,7 +38,7 @@ export default function Skip_static_subtree($$anchor, $$props) {
var select = $.sibling(div_1, 2);
var option = $.child(select);
- option.value = null == (option.__value = 'a') ? '' : 'a';
+ option.value = option.__value = 'a';
$.reset(select);
var img = $.sibling(select, 2);
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 e694c12647dc..0532ec5aa985 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
a `;
+ $$payload.out += ` ${$.escape(title)} we don't need to traverse these nodes
or
these
ones
${$.html(content)} these
trailing
nodes
can
be
completely
ignored
a `;
}
\ 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 a67210e5418a..c446b3d3ef1f 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(` reset `, 1);
+var root = $.from_html(` reset `, 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 7b2a884d7044..f814dd4f84a1 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 += ` reset `;
+ $$payload.out += ` reset `;
}
\ 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 d520d1ef2488..464435cb0a63 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/sourcemaps/samples/attached-sourcemap/input.svelte b/packages/svelte/tests/sourcemaps/samples/attached-sourcemap/input.svelte
index 21a47a72a9c9..715bbda8d92e 100644
--- a/packages/svelte/tests/sourcemaps/samples/attached-sourcemap/input.svelte
+++ b/packages/svelte/tests/sourcemaps/samples/attached-sourcemap/input.svelte
@@ -8,4 +8,4 @@
replace_me_script = 'hello'
;
-{done_replace_script_2}
+{Math.random() < 1 && done_replace_script_2}
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 000000000000..21587e5f4fe8
--- /dev/null
+++ b/packages/svelte/tests/validator/samples/bidirectional-control-characters/input.svelte
@@ -0,0 +1,8 @@
+
+defabc
+Hello, {name}!
+
+
+defabc
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 000000000000..6e70193c6c83
--- /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 000000000000..82765c51c1c4
--- /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 000000000000..05cd4d9d9d64
--- /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 000000000000..c4cb0991d093
--- /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 000000000000..e5ad562727c7
--- /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 000000000000..82765c51c1c4
--- /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 000000000000..e37be4b3e691
--- /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 000000000000..175c41f98c1b
--- /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 000000000000..f9196ff3cd51
--- /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 000000000000..9f959874c80e
--- /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 000000000000..bf1aada1b5df
--- /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 000000000000..af2f30dadee5
--- /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 000000000000..bc3d19a14fae
--- /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 000000000000..ae7a47f31bbd
--- /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 000000000000..2ebe52e685ed
--- /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 000000000000..64e56f8d5c4e
--- /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 000000000000..50c855983700
--- /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 000000000000..2e0bd10ff899
--- /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 000000000000..0a76c6fec90f
--- /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 000000000000..b7dd4c8ed457
--- /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 000000000000..a8469e13af46
--- /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 32594e426846..e1906b181a4d 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 000000000000..bb7b930dc328
--- /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 000000000000..b880fe146ccb
--- /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 000000000000..207b554527a1
--- /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 000000000000..61e11ab10804
--- /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 000000000000..ca5b16f8c38c
--- /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 000000000000..4c50bbd11632
--- /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 000000000000..f67eba18b8aa
--- /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 000000000000..1316a2b65b18
--- /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 000000000000..7721f2f3802a
--- /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 000000000000..6ea36c5a50f1
--- /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/tests/validator/samples/invalid-self-closing-tag/input.svelte b/packages/svelte/tests/validator/samples/invalid-self-closing-tag/input.svelte
index 376c9f79bd84..07582e834341 100644
--- a/packages/svelte/tests/validator/samples/invalid-self-closing-tag/input.svelte
+++ b/packages/svelte/tests/validator/samples/invalid-self-closing-tag/input.svelte
@@ -1,6 +1,7 @@
+
diff --git a/packages/svelte/tests/validator/samples/invalid-self-closing-tag/warnings.json b/packages/svelte/tests/validator/samples/invalid-self-closing-tag/warnings.json
index 40b87ec7c8f4..3e45bca5ad15 100644
--- a/packages/svelte/tests/validator/samples/invalid-self-closing-tag/warnings.json
+++ b/packages/svelte/tests/validator/samples/invalid-self-closing-tag/warnings.json
@@ -3,11 +3,11 @@
"code": "element_invalid_self_closing_tag",
"message": "Self-closing HTML tags for non-void elements are ambiguous — use `
` rather than `
`",
"start": {
- "line": 8,
+ "line": 9,
"column": 0
},
"end": {
- "line": 8,
+ "line": 9,
"column": 7
}
},
@@ -15,11 +15,11 @@
"code": "element_invalid_self_closing_tag",
"message": "Self-closing HTML tags for non-void elements are ambiguous — use ` ` rather than ` `",
"start": {
- "line": 9,
+ "line": 10,
"column": 0
},
"end": {
- "line": 9,
+ "line": 10,
"column": 12
}
}
diff --git a/packages/svelte/tests/validator/samples/reassign-derived-literal/errors.json b/packages/svelte/tests/validator/samples/reassign-derived-literal/errors.json
deleted file mode 100644
index 8681d84ab227..000000000000
--- a/packages/svelte/tests/validator/samples/reassign-derived-literal/errors.json
+++ /dev/null
@@ -1,14 +0,0 @@
-[
- {
- "code": "constant_assignment",
- "message": "Cannot assign to derived state",
- "start": {
- "column": 3,
- "line": 6
- },
- "end": {
- "column": 29,
- "line": 6
- }
- }
-]
diff --git a/packages/svelte/tests/validator/samples/reassign-derived-literal/input.svelte b/packages/svelte/tests/validator/samples/reassign-derived-literal/input.svelte
deleted file mode 100644
index 8f109c9e1fe8..000000000000
--- a/packages/svelte/tests/validator/samples/reassign-derived-literal/input.svelte
+++ /dev/null
@@ -1,9 +0,0 @@
-
\ No newline at end of file
diff --git a/packages/svelte/tests/validator/samples/reassign-derived-private-field/errors.json b/packages/svelte/tests/validator/samples/reassign-derived-private-field/errors.json
deleted file mode 100644
index c211aa460895..000000000000
--- a/packages/svelte/tests/validator/samples/reassign-derived-private-field/errors.json
+++ /dev/null
@@ -1,14 +0,0 @@
-[
- {
- "code": "constant_assignment",
- "message": "Cannot assign to derived state",
- "start": {
- "column": 3,
- "line": 6
- },
- "end": {
- "column": 27,
- "line": 6
- }
- }
-]
diff --git a/packages/svelte/tests/validator/samples/reassign-derived-private-field/input.svelte b/packages/svelte/tests/validator/samples/reassign-derived-private-field/input.svelte
deleted file mode 100644
index 62e2317e033f..000000000000
--- a/packages/svelte/tests/validator/samples/reassign-derived-private-field/input.svelte
+++ /dev/null
@@ -1,9 +0,0 @@
-
\ No newline at end of file
diff --git a/packages/svelte/tests/validator/samples/reassign-derived-public-field/errors.json b/packages/svelte/tests/validator/samples/reassign-derived-public-field/errors.json
deleted file mode 100644
index 98837589ac80..000000000000
--- a/packages/svelte/tests/validator/samples/reassign-derived-public-field/errors.json
+++ /dev/null
@@ -1,14 +0,0 @@
-[
- {
- "code": "constant_assignment",
- "message": "Cannot assign to derived state",
- "start": {
- "column": 3,
- "line": 6
- },
- "end": {
- "column": 26,
- "line": 6
- }
- }
-]
diff --git a/packages/svelte/tests/validator/samples/reassign-derived-public-field/input.svelte b/packages/svelte/tests/validator/samples/reassign-derived-public-field/input.svelte
deleted file mode 100644
index e2c4693e86b7..000000000000
--- a/packages/svelte/tests/validator/samples/reassign-derived-public-field/input.svelte
+++ /dev/null
@@ -1,9 +0,0 @@
-
\ No newline at end of file
diff --git a/packages/svelte/tests/validator/samples/rune-invalid-spread-derived-by/errors.json b/packages/svelte/tests/validator/samples/rune-invalid-spread-derived-by/errors.json
new file mode 100644
index 000000000000..be59da95fa6a
--- /dev/null
+++ b/packages/svelte/tests/validator/samples/rune-invalid-spread-derived-by/errors.json
@@ -0,0 +1,14 @@
+[
+ {
+ "code": "rune_invalid_spread",
+ "end": {
+ "column": 35,
+ "line": 3
+ },
+ "message": "`$derived.by` cannot be called with a spread argument",
+ "start": {
+ "column": 15,
+ "line": 3
+ }
+ }
+]
diff --git a/packages/svelte/tests/validator/samples/rune-invalid-spread-derived-by/input.svelte b/packages/svelte/tests/validator/samples/rune-invalid-spread-derived-by/input.svelte
new file mode 100644
index 000000000000..49e8057aa508
--- /dev/null
+++ b/packages/svelte/tests/validator/samples/rune-invalid-spread-derived-by/input.svelte
@@ -0,0 +1,4 @@
+
\ No newline at end of file
diff --git a/packages/svelte/tests/validator/samples/rune-invalid-spread-derived/errors.json b/packages/svelte/tests/validator/samples/rune-invalid-spread-derived/errors.json
new file mode 100644
index 000000000000..6a333bc36233
--- /dev/null
+++ b/packages/svelte/tests/validator/samples/rune-invalid-spread-derived/errors.json
@@ -0,0 +1,14 @@
+[
+ {
+ "code": "rune_invalid_spread",
+ "end": {
+ "column": 32,
+ "line": 3
+ },
+ "message": "`$derived` cannot be called with a spread argument",
+ "start": {
+ "column": 15,
+ "line": 3
+ }
+ }
+]
diff --git a/packages/svelte/tests/validator/samples/rune-invalid-spread-derived/input.svelte b/packages/svelte/tests/validator/samples/rune-invalid-spread-derived/input.svelte
new file mode 100644
index 000000000000..9155493e1705
--- /dev/null
+++ b/packages/svelte/tests/validator/samples/rune-invalid-spread-derived/input.svelte
@@ -0,0 +1,4 @@
+
\ No newline at end of file
diff --git a/packages/svelte/tests/validator/samples/rune-invalid-spread-state-raw/errors.json b/packages/svelte/tests/validator/samples/rune-invalid-spread-state-raw/errors.json
new file mode 100644
index 000000000000..e08b498fcbee
--- /dev/null
+++ b/packages/svelte/tests/validator/samples/rune-invalid-spread-state-raw/errors.json
@@ -0,0 +1,14 @@
+[
+ {
+ "code": "rune_invalid_spread",
+ "end": {
+ "column": 34,
+ "line": 3
+ },
+ "message": "`$state.raw` cannot be called with a spread argument",
+ "start": {
+ "column": 15,
+ "line": 3
+ }
+ }
+]
diff --git a/packages/svelte/tests/validator/samples/rune-invalid-spread-state-raw/input.svelte b/packages/svelte/tests/validator/samples/rune-invalid-spread-state-raw/input.svelte
new file mode 100644
index 000000000000..d06fb053b3d9
--- /dev/null
+++ b/packages/svelte/tests/validator/samples/rune-invalid-spread-state-raw/input.svelte
@@ -0,0 +1,4 @@
+
\ No newline at end of file
diff --git a/packages/svelte/tests/validator/samples/rune-invalid-spread-state/errors.json b/packages/svelte/tests/validator/samples/rune-invalid-spread-state/errors.json
new file mode 100644
index 000000000000..11ae2abce538
--- /dev/null
+++ b/packages/svelte/tests/validator/samples/rune-invalid-spread-state/errors.json
@@ -0,0 +1,14 @@
+[
+ {
+ "code": "rune_invalid_spread",
+ "end": {
+ "column": 30,
+ "line": 3
+ },
+ "message": "`$state` cannot be called with a spread argument",
+ "start": {
+ "column": 15,
+ "line": 3
+ }
+ }
+]
diff --git a/packages/svelte/tests/validator/samples/rune-invalid-spread-state/input.svelte b/packages/svelte/tests/validator/samples/rune-invalid-spread-state/input.svelte
new file mode 100644
index 000000000000..02feac893f6d
--- /dev/null
+++ b/packages/svelte/tests/validator/samples/rune-invalid-spread-state/input.svelte
@@ -0,0 +1,4 @@
+
\ No newline at end of file
diff --git a/packages/svelte/tests/validator/samples/static-state-reference/input.svelte b/packages/svelte/tests/validator/samples/static-state-reference/input.svelte
index cd0c7b734925..577527ee60c7 100644
--- a/packages/svelte/tests/validator/samples/static-state-reference/input.svelte
+++ b/packages/svelte/tests/validator/samples/static-state-reference/input.svelte
@@ -2,6 +2,7 @@
let obj = $state({ a: 0 });
let count = $state(0);
let doubled = $derived(count * 2);
+ let tripled = $state(count * 3);
console.log(obj);
console.log(count);
diff --git a/packages/svelte/tests/validator/samples/static-state-reference/warnings.json b/packages/svelte/tests/validator/samples/static-state-reference/warnings.json
index 9ba09415190b..a118d5e4a0d7 100644
--- a/packages/svelte/tests/validator/samples/static-state-reference/warnings.json
+++ b/packages/svelte/tests/validator/samples/static-state-reference/warnings.json
@@ -1,26 +1,38 @@
[
{
"code": "state_referenced_locally",
- "message": "State referenced in its own scope will never update. Did you mean to reference it inside a closure?",
+ "message": "This reference only captures the initial value of `count`. Did you mean to reference it inside a derived instead?",
+ "start": {
+ "column": 22,
+ "line": 5
+ },
+ "end": {
+ "column": 27,
+ "line": 5
+ }
+ },
+ {
+ "code": "state_referenced_locally",
+ "message": "This reference only captures the initial value of `count`. Did you mean to reference it inside a closure instead?",
"start": {
"column": 13,
- "line": 7
+ "line": 8
},
"end": {
"column": 18,
- "line": 7
+ "line": 8
}
},
{
"code": "state_referenced_locally",
- "message": "State referenced in its own scope will never update. Did you mean to reference it inside a closure?",
+ "message": "This reference only captures the initial value of `doubled`. Did you mean to reference it inside a closure instead?",
"start": {
"column": 13,
- "line": 8
+ "line": 9
},
"end": {
"column": 20,
- "line": 8
+ "line": 9
}
}
]
diff --git a/packages/svelte/tsconfig.json b/packages/svelte/tsconfig.json
index 76005add13be..bab587ace3b1 100644
--- a/packages/svelte/tsconfig.json
+++ b/packages/svelte/tsconfig.json
@@ -24,11 +24,7 @@
"svelte/motion": ["./src/motion/public.d.ts"],
"svelte/server": ["./src/server/index.d.ts"],
"svelte/store": ["./src/store/public.d.ts"],
- "svelte/reactivity": ["./src/reactivity/index-client.js"],
- "#compiler": ["./src/compiler/types/index.d.ts"],
- "#client": ["./src/internal/client/types.d.ts"],
- "#server": ["./src/internal/server/types.d.ts"],
- "#shared": ["./src/internal/shared/types.d.ts"]
+ "svelte/reactivity": ["./src/reactivity/index-client.js"]
}
},
"include": [
diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts
index c3dbdcac791e..432171ae0dd1 100644
--- a/packages/svelte/types/index.d.ts
+++ b/packages/svelte/types/index.d.ts
@@ -349,13 +349,38 @@ declare module 'svelte' {
props: Props;
});
/**
- * The `onMount` function schedules a callback to run as soon as the component has been mounted to the DOM.
- * It must be called during the component's initialisation (but doesn't need to live *inside* the component;
- * it can be called from an external module).
+ * Returns an [`AbortSignal`](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal) that aborts when the current [derived](https://svelte.dev/docs/svelte/$derived) or [effect](https://svelte.dev/docs/svelte/$effect) re-runs or is destroyed.
*
- * If a function is returned _synchronously_ from `onMount`, it will be called when the component is unmounted.
+ * Must be called while a derived or effect is running.
*
- * `onMount` does not run inside [server-side components](https://svelte.dev/docs/svelte/svelte-server#render).
+ * ```svelte
+ *
+ * ```
+ */
+ export function getAbortSignal(): AbortSignal;
+ /**
+ * `onMount`, like [`$effect`](https://svelte.dev/docs/svelte/$effect), schedules a function to run as soon as the component has been mounted to the DOM.
+ * Unlike `$effect`, the provided function only runs once.
+ *
+ * It must be called during the component's initialisation (but doesn't need to live _inside_ the component;
+ * it can be called from an external module). If a function is returned _synchronously_ from `onMount`,
+ * it will be called when the component is unmounted.
+ *
+ * `onMount` functions do not run during [server-side rendering](https://svelte.dev/docs/svelte/svelte-server#render).
*
* */
export function onMount(fn: () => NotFunction | Promise> | (() => any)): void;
@@ -380,7 +405,7 @@ declare module 'svelte' {
* The event dispatcher can be typed to narrow the allowed event names and the type of the `detail` argument:
* ```ts
* const dispatch = createEventDispatcher<{
- * loaded: never; // does not take a detail argument
+ * loaded: null; // does not take a detail argument
* change: string; // takes a detail argument of type string, which is required
* optional: number | null; // takes an optional detail argument of type number
* }>();
@@ -623,6 +648,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
+ *
+ *
+ * click me
+ * ```
+ * @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';
@@ -752,6 +916,8 @@ declare module 'svelte/compiler' {
code: string;
/** A source map */
map: SourceMap;
+ /** Whether or not the CSS includes global rules */
+ hasGlobal: boolean;
};
/**
* An array of warning objects that were generated during compilation. Each warning has several properties:
@@ -843,6 +1009,16 @@ declare module 'svelte/compiler' {
* @default false
*/
preserveWhitespace?: boolean;
+ /**
+ * Which strategy to use when cloning DOM fragments:
+ *
+ * - `html` populates a `` with `innerHTML` and clones it. This is faster, but cannot be used if your app's [Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CSP) includes [`require-trusted-types-for 'script'`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy/require-trusted-types-for)
+ * - `tree` creates the fragment one element at a time and _then_ clones it. This is slower, but works everywhere
+ *
+ * @default 'html'
+ * @since 5.33
+ */
+ fragments?: 'html' | 'tree';
/**
* Set to `true` to force the compiler into runes mode, even if there are no indications of runes usage.
* Set to `false` to force the compiler into ignoring runes, even if there are indications of runes usage.
@@ -968,6 +1144,8 @@ declare module 'svelte/compiler' {
instance: Script | null;
/** The parsed `
+ *
+ * The time is {formatter.format(date)}
+ * ```
+ */
export class SvelteDate extends Date {
constructor(...params: any[]);
#private;
}
+ /**
+ * A reactive version of the built-in [`Set`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set) object.
+ * Reading contents of the set (by iterating, or by reading `set.size` or calling `set.has(...)` as in the [example](https://svelte.dev/playground/53438b51194b4882bcc18cddf9f96f15) below) in an [effect](https://svelte.dev/docs/svelte/$effect) or [derived](https://svelte.dev/docs/svelte/$derived)
+ * will cause it to be re-evaluated as necessary when the set is updated.
+ *
+ * Note that values in a reactive set are _not_ made [deeply reactive](https://svelte.dev/docs/svelte/$state#Deep-state).
+ *
+ * ```svelte
+ *
+ *
+ * {#each ['🙈', '🙉', '🙊'] as monkey}
+ * toggle(monkey)}>{monkey}
+ * {/each}
+ *
+ * monkeys.clear()}>clear
+ *
+ * {#if monkeys.has('🙈')}see no evil
{/if}
+ * {#if monkeys.has('🙉')}hear no evil
{/if}
+ * {#if monkeys.has('🙊')}speak no evil
{/if}
+ * ```
+ *
+ *
+ */
export class SvelteSet extends Set {
constructor(value?: Iterable | null | undefined);
@@ -1909,6 +2178,50 @@ declare module 'svelte/reactivity' {
add(value: T): this;
#private;
}
+ /**
+ * A reactive version of the built-in [`Map`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map) object.
+ * Reading contents of the map (by iterating, or by reading `map.size` or calling `map.get(...)` or `map.has(...)` as in the [tic-tac-toe example](https://svelte.dev/playground/0b0ff4aa49c9443f9b47fe5203c78293) below) in an [effect](https://svelte.dev/docs/svelte/$effect) or [derived](https://svelte.dev/docs/svelte/$derived)
+ * will cause it to be re-evaluated as necessary when the map is updated.
+ *
+ * Note that values in a reactive map are _not_ made [deeply reactive](https://svelte.dev/docs/svelte/$state#Deep-state).
+ *
+ * ```svelte
+ *
+ *
+ *
+ * {#each Array(9), i}
+ * {
+ * board.set(i, player);
+ * player = player === 'x' ? 'o' : 'x';
+ * }}
+ * >{board.get(i)}
+ * {/each}
+ *
+ *
+ * {#if winner}
+ * {winner} wins!
+ * reset
+ * {:else}
+ * {player} is next
+ * {/if}
+ * ```
+ *
+ *
+ */
export class SvelteMap extends Map {
constructor(value?: Iterable | null | undefined);
@@ -1916,11 +2229,64 @@ declare module 'svelte/reactivity' {
set(key: K, value: V): this;
#private;
}
+ /**
+ * A reactive version of the built-in [`URL`](https://developer.mozilla.org/en-US/docs/Web/API/URL) object.
+ * Reading properties of the URL (such as `url.href` or `url.pathname`) in an [effect](https://svelte.dev/docs/svelte/$effect) or [derived](https://svelte.dev/docs/svelte/$derived)
+ * will cause it to be re-evaluated as necessary when the URL changes.
+ *
+ * The `searchParams` property is an instance of [SvelteURLSearchParams](https://svelte.dev/docs/svelte/svelte-reactivity#SvelteURLSearchParams).
+ *
+ * [Example](https://svelte.dev/playground/5a694758901b448c83dc40dc31c71f2a):
+ *
+ * ```svelte
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ * ```
+ */
export class SvelteURL extends URL {
get searchParams(): SvelteURLSearchParams;
#private;
}
const REPLACE: unique symbol;
+ /**
+ * A reactive version of the built-in [`URLSearchParams`](https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams) object.
+ * Reading its contents (by iterating, or by calling `params.get(...)` or `params.getAll(...)` as in the [example](https://svelte.dev/playground/b3926c86c5384bab9f2cf993bc08c1c8) below) in an [effect](https://svelte.dev/docs/svelte/$effect) or [derived](https://svelte.dev/docs/svelte/$derived)
+ * will cause it to be re-evaluated as necessary when the params are updated.
+ *
+ * ```svelte
+ *
+ *
+ *
+ *
+ * params.append(key, value)}>append
+ *
+ * ?{params.toString()}
+ *
+ * {#each params as [key, value]}
+ * {key}: {value}
+ * {/each}
+ * ```
+ */
export class SvelteURLSearchParams extends URLSearchParams {
[REPLACE](params: URLSearchParams): void;
@@ -2553,6 +2919,16 @@ declare module 'svelte/types/compiler/interfaces' {
* @default false
*/
preserveWhitespace?: boolean;
+ /**
+ * Which strategy to use when cloning DOM fragments:
+ *
+ * - `html` populates a `` with `innerHTML` and clones it. This is faster, but cannot be used if your app's [Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CSP) includes [`require-trusted-types-for 'script'`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy/require-trusted-types-for)
+ * - `tree` creates the fragment one element at a time and _then_ clones it. This is slower, but works everywhere
+ *
+ * @default 'html'
+ * @since 5.33
+ */
+ fragments?: 'html' | 'tree';
/**
* Set to `true` to force the compiler into runes mode, even if there are no indications of runes usage.
* Set to `false` to force the compiler into ignoring runes, even if there are indications of runes usage.
diff --git a/playgrounds/sandbox/package.json b/playgrounds/sandbox/package.json
index 9e2e5f673815..3ab65ac4b50f 100644
--- a/playgrounds/sandbox/package.json
+++ b/playgrounds/sandbox/package.json
@@ -18,7 +18,7 @@
"polka": "^1.0.0-next.25",
"svelte": "workspace:*",
"tinyglobby": "^0.2.12",
- "vite": "^5.4.14",
+ "vite": "^5.4.19",
"vite-plugin-inspect": "^0.8.4"
}
}
diff --git a/playgrounds/sandbox/run.js b/playgrounds/sandbox/run.js
index 8fa2c2a2dac4..2029937f52dc 100644
--- a/playgrounds/sandbox/run.js
+++ b/playgrounds/sandbox/run.js
@@ -56,7 +56,7 @@ for (const generate of /** @type {const} */ (['client', 'server'])) {
});
write(
- `${cwd}/output/${file}.json`,
+ `${cwd}/output/ast/${file}.json`,
JSON.stringify(
ast,
(key, value) => (typeof value === 'bigint' ? ['BigInt', value.toString()] : value),
@@ -66,14 +66,14 @@ for (const generate of /** @type {const} */ (['client', 'server'])) {
try {
const migrated = migrate(source);
- write(`${cwd}/output/${file}.migrated.svelte`, migrated.code);
+ write(`${cwd}/output/migrated/${file}`, migrated.code);
} catch (e) {
console.warn(`Error migrating ${file}`, e);
}
}
const compiled = compile(source, {
- dev: true,
+ dev: false,
filename: input,
generate,
runes: argv.values.runes
@@ -85,9 +85,25 @@ for (const generate of /** @type {const} */ (['client', 'server'])) {
}
write(output_js, compiled.js.code + '\n//# sourceMappingURL=' + path.basename(output_map));
-
write(output_map, compiled.js.map.toString());
+ // generate with fragments: 'tree'
+ if (generate === 'client') {
+ const compiled = compile(source, {
+ dev: false,
+ filename: input,
+ generate,
+ runes: argv.values.runes,
+ fragments: 'tree'
+ });
+
+ const output_js = `${cwd}/output/${generate}/${file}.tree.js`;
+ const output_map = `${cwd}/output/${generate}/${file}.tree.js.map`;
+
+ write(output_js, compiled.js.code + '\n//# sourceMappingURL=' + path.basename(output_map));
+ write(output_map, compiled.js.map.toString());
+ }
+
if (compiled.css) {
write(output_css, compiled.css.code);
}
@@ -98,7 +114,7 @@ for (const generate of /** @type {const} */ (['client', 'server'])) {
const source = fs.readFileSync(input, 'utf-8');
const compiled = compileModule(source, {
- dev: true,
+ dev: false,
filename: input,
generate
});
diff --git a/playgrounds/sandbox/svelte.config.js b/playgrounds/sandbox/svelte.config.js
new file mode 100644
index 000000000000..65f739bdb608
--- /dev/null
+++ b/playgrounds/sandbox/svelte.config.js
@@ -0,0 +1,5 @@
+export default {
+ compilerOptions: {
+ hmr: true
+ }
+};
diff --git a/playgrounds/sandbox/vite.config.js b/playgrounds/sandbox/vite.config.js
index 51bfd0a2122e..5ce020421709 100644
--- a/playgrounds/sandbox/vite.config.js
+++ b/playgrounds/sandbox/vite.config.js
@@ -7,14 +7,7 @@ export default defineConfig({
minify: false
},
- plugins: [
- inspect(),
- svelte({
- compilerOptions: {
- hmr: true
- }
- })
- ],
+ plugins: [inspect(), svelte()],
optimizeDeps: {
// svelte is a local workspace package, optimizing it would require dev server restarts with --force for every change
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index c687db12d4a9..87fb2b2667b2 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -87,8 +87,8 @@ importers:
specifier: ^1.2.1
version: 1.2.1
esrap:
- specifier: ^1.4.3
- version: 1.4.3
+ specifier: ^2.1.0
+ version: 2.1.0
is-reference:
specifier: ^3.0.3
version: 3.0.3
@@ -152,7 +152,7 @@ importers:
devDependencies:
'@sveltejs/vite-plugin-svelte':
specifier: ^4.0.0-next.6
- version: 4.0.0-next.6(svelte@packages+svelte)(vite@5.4.14(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0))
+ version: 4.0.0-next.6(svelte@packages+svelte)(vite@5.4.19(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0))
polka:
specifier: ^1.0.0-next.25
version: 1.0.0-next.25
@@ -163,11 +163,11 @@ importers:
specifier: ^0.2.12
version: 0.2.12
vite:
- specifier: ^5.4.14
- version: 5.4.14(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)
+ specifier: ^5.4.19
+ version: 5.4.19(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)
vite-plugin-inspect:
specifier: ^0.8.4
- version: 0.8.4(rollup@4.22.4)(vite@5.4.14(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0))
+ version: 0.8.4(rollup@4.40.2)(vite@5.4.19(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0))
packages:
@@ -408,6 +408,12 @@ packages:
peerDependencies:
eslint: ^6.0.0 || ^7.0.0 || >=8.0.0
+ '@eslint-community/eslint-utils@4.7.0':
+ resolution: {integrity: sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==}
+ engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
+ peerDependencies:
+ eslint: ^6.0.0 || ^7.0.0 || >=8.0.0
+
'@eslint-community/regexpp@4.12.1':
resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==}
engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0}
@@ -545,81 +551,181 @@ packages:
cpu: [arm]
os: [android]
+ '@rollup/rollup-android-arm-eabi@4.40.2':
+ resolution: {integrity: sha512-JkdNEq+DFxZfUwxvB58tHMHBHVgX23ew41g1OQinthJ+ryhdRk67O31S7sYw8u2lTjHUPFxwar07BBt1KHp/hg==}
+ cpu: [arm]
+ os: [android]
+
'@rollup/rollup-android-arm64@4.22.4':
resolution: {integrity: sha512-VXoK5UMrgECLYaMuGuVTOx5kcuap1Jm8g/M83RnCHBKOqvPPmROFJGQaZhGccnsFtfXQ3XYa4/jMCJvZnbJBdA==}
cpu: [arm64]
os: [android]
+ '@rollup/rollup-android-arm64@4.40.2':
+ resolution: {integrity: sha512-13unNoZ8NzUmnndhPTkWPWbX3vtHodYmy+I9kuLxN+F+l+x3LdVF7UCu8TWVMt1POHLh6oDHhnOA04n8oJZhBw==}
+ cpu: [arm64]
+ os: [android]
+
'@rollup/rollup-darwin-arm64@4.22.4':
resolution: {integrity: sha512-xMM9ORBqu81jyMKCDP+SZDhnX2QEVQzTcC6G18KlTQEzWK8r/oNZtKuZaCcHhnsa6fEeOBionoyl5JsAbE/36Q==}
cpu: [arm64]
os: [darwin]
+ '@rollup/rollup-darwin-arm64@4.40.2':
+ resolution: {integrity: sha512-Gzf1Hn2Aoe8VZzevHostPX23U7N5+4D36WJNHK88NZHCJr7aVMG4fadqkIf72eqVPGjGc0HJHNuUaUcxiR+N/w==}
+ cpu: [arm64]
+ os: [darwin]
+
'@rollup/rollup-darwin-x64@4.22.4':
resolution: {integrity: sha512-aJJyYKQwbHuhTUrjWjxEvGnNNBCnmpHDvrb8JFDbeSH3m2XdHcxDd3jthAzvmoI8w/kSjd2y0udT+4okADsZIw==}
cpu: [x64]
os: [darwin]
+ '@rollup/rollup-darwin-x64@4.40.2':
+ resolution: {integrity: sha512-47N4hxa01a4x6XnJoskMKTS8XZ0CZMd8YTbINbi+w03A2w4j1RTlnGHOz/P0+Bg1LaVL6ufZyNprSg+fW5nYQQ==}
+ cpu: [x64]
+ os: [darwin]
+
+ '@rollup/rollup-freebsd-arm64@4.40.2':
+ resolution: {integrity: sha512-8t6aL4MD+rXSHHZUR1z19+9OFJ2rl1wGKvckN47XFRVO+QL/dUSpKA2SLRo4vMg7ELA8pzGpC+W9OEd1Z/ZqoQ==}
+ cpu: [arm64]
+ os: [freebsd]
+
+ '@rollup/rollup-freebsd-x64@4.40.2':
+ resolution: {integrity: sha512-C+AyHBzfpsOEYRFjztcYUFsH4S7UsE9cDtHCtma5BK8+ydOZYgMmWg1d/4KBytQspJCld8ZIujFMAdKG1xyr4Q==}
+ cpu: [x64]
+ os: [freebsd]
+
'@rollup/rollup-linux-arm-gnueabihf@4.22.4':
resolution: {integrity: sha512-j63YtCIRAzbO+gC2L9dWXRh5BFetsv0j0va0Wi9epXDgU/XUi5dJKo4USTttVyK7fGw2nPWK0PbAvyliz50SCQ==}
cpu: [arm]
os: [linux]
+ '@rollup/rollup-linux-arm-gnueabihf@4.40.2':
+ resolution: {integrity: sha512-de6TFZYIvJwRNjmW3+gaXiZ2DaWL5D5yGmSYzkdzjBDS3W+B9JQ48oZEsmMvemqjtAFzE16DIBLqd6IQQRuG9Q==}
+ cpu: [arm]
+ os: [linux]
+
'@rollup/rollup-linux-arm-musleabihf@4.22.4':
resolution: {integrity: sha512-dJnWUgwWBX1YBRsuKKMOlXCzh2Wu1mlHzv20TpqEsfdZLb3WoJW2kIEsGwLkroYf24IrPAvOT/ZQ2OYMV6vlrg==}
cpu: [arm]
os: [linux]
+ '@rollup/rollup-linux-arm-musleabihf@4.40.2':
+ resolution: {integrity: sha512-urjaEZubdIkacKc930hUDOfQPysezKla/O9qV+O89enqsqUmQm8Xj8O/vh0gHg4LYfv7Y7UsE3QjzLQzDYN1qg==}
+ cpu: [arm]
+ os: [linux]
+
'@rollup/rollup-linux-arm64-gnu@4.22.4':
resolution: {integrity: sha512-AdPRoNi3NKVLolCN/Sp4F4N1d98c4SBnHMKoLuiG6RXgoZ4sllseuGioszumnPGmPM2O7qaAX/IJdeDU8f26Aw==}
cpu: [arm64]
os: [linux]
+ '@rollup/rollup-linux-arm64-gnu@4.40.2':
+ resolution: {integrity: sha512-KlE8IC0HFOC33taNt1zR8qNlBYHj31qGT1UqWqtvR/+NuCVhfufAq9fxO8BMFC22Wu0rxOwGVWxtCMvZVLmhQg==}
+ cpu: [arm64]
+ os: [linux]
+
'@rollup/rollup-linux-arm64-musl@4.22.4':
resolution: {integrity: sha512-Gl0AxBtDg8uoAn5CCqQDMqAx22Wx22pjDOjBdmG0VIWX3qUBHzYmOKh8KXHL4UpogfJ14G4wk16EQogF+v8hmA==}
cpu: [arm64]
os: [linux]
+ '@rollup/rollup-linux-arm64-musl@4.40.2':
+ resolution: {integrity: sha512-j8CgxvfM0kbnhu4XgjnCWJQyyBOeBI1Zq91Z850aUddUmPeQvuAy6OiMdPS46gNFgy8gN1xkYyLgwLYZG3rBOg==}
+ cpu: [arm64]
+ os: [linux]
+
+ '@rollup/rollup-linux-loongarch64-gnu@4.40.2':
+ resolution: {integrity: sha512-Ybc/1qUampKuRF4tQXc7G7QY9YRyeVSykfK36Y5Qc5dmrIxwFhrOzqaVTNoZygqZ1ZieSWTibfFhQ5qK8jpWxw==}
+ cpu: [loong64]
+ os: [linux]
+
'@rollup/rollup-linux-powerpc64le-gnu@4.22.4':
resolution: {integrity: sha512-3aVCK9xfWW1oGQpTsYJJPF6bfpWfhbRnhdlyhak2ZiyFLDaayz0EP5j9V1RVLAAxlmWKTDfS9wyRyY3hvhPoOg==}
cpu: [ppc64]
os: [linux]
+ '@rollup/rollup-linux-powerpc64le-gnu@4.40.2':
+ resolution: {integrity: sha512-3FCIrnrt03CCsZqSYAOW/k9n625pjpuMzVfeI+ZBUSDT3MVIFDSPfSUgIl9FqUftxcUXInvFah79hE1c9abD+Q==}
+ cpu: [ppc64]
+ os: [linux]
+
'@rollup/rollup-linux-riscv64-gnu@4.22.4':
resolution: {integrity: sha512-ePYIir6VYnhgv2C5Xe9u+ico4t8sZWXschR6fMgoPUK31yQu7hTEJb7bCqivHECwIClJfKgE7zYsh1qTP3WHUA==}
cpu: [riscv64]
os: [linux]
+ '@rollup/rollup-linux-riscv64-gnu@4.40.2':
+ resolution: {integrity: sha512-QNU7BFHEvHMp2ESSY3SozIkBPaPBDTsfVNGx3Xhv+TdvWXFGOSH2NJvhD1zKAT6AyuuErJgbdvaJhYVhVqrWTg==}
+ cpu: [riscv64]
+ os: [linux]
+
+ '@rollup/rollup-linux-riscv64-musl@4.40.2':
+ resolution: {integrity: sha512-5W6vNYkhgfh7URiXTO1E9a0cy4fSgfE4+Hl5agb/U1sa0kjOLMLC1wObxwKxecE17j0URxuTrYZZME4/VH57Hg==}
+ cpu: [riscv64]
+ os: [linux]
+
'@rollup/rollup-linux-s390x-gnu@4.22.4':
resolution: {integrity: sha512-GqFJ9wLlbB9daxhVlrTe61vJtEY99/xB3C8e4ULVsVfflcpmR6c8UZXjtkMA6FhNONhj2eA5Tk9uAVw5orEs4Q==}
cpu: [s390x]
os: [linux]
+ '@rollup/rollup-linux-s390x-gnu@4.40.2':
+ resolution: {integrity: sha512-B7LKIz+0+p348JoAL4X/YxGx9zOx3sR+o6Hj15Y3aaApNfAshK8+mWZEf759DXfRLeL2vg5LYJBB7DdcleYCoQ==}
+ cpu: [s390x]
+ os: [linux]
+
'@rollup/rollup-linux-x64-gnu@4.22.4':
resolution: {integrity: sha512-87v0ol2sH9GE3cLQLNEy0K/R0pz1nvg76o8M5nhMR0+Q+BBGLnb35P0fVz4CQxHYXaAOhE8HhlkaZfsdUOlHwg==}
cpu: [x64]
os: [linux]
+ '@rollup/rollup-linux-x64-gnu@4.40.2':
+ resolution: {integrity: sha512-lG7Xa+BmBNwpjmVUbmyKxdQJ3Q6whHjMjzQplOs5Z+Gj7mxPtWakGHqzMqNER68G67kmCX9qX57aRsW5V0VOng==}
+ cpu: [x64]
+ os: [linux]
+
'@rollup/rollup-linux-x64-musl@4.22.4':
resolution: {integrity: sha512-UV6FZMUgePDZrFjrNGIWzDo/vABebuXBhJEqrHxrGiU6HikPy0Z3LfdtciIttEUQfuDdCn8fqh7wiFJjCNwO+g==}
cpu: [x64]
os: [linux]
+ '@rollup/rollup-linux-x64-musl@4.40.2':
+ resolution: {integrity: sha512-tD46wKHd+KJvsmije4bUskNuvWKFcTOIM9tZ/RrmIvcXnbi0YK/cKS9FzFtAm7Oxi2EhV5N2OpfFB348vSQRXA==}
+ cpu: [x64]
+ os: [linux]
+
'@rollup/rollup-win32-arm64-msvc@4.22.4':
resolution: {integrity: sha512-BjI+NVVEGAXjGWYHz/vv0pBqfGoUH0IGZ0cICTn7kB9PyjrATSkX+8WkguNjWoj2qSr1im/+tTGRaY+4/PdcQw==}
cpu: [arm64]
os: [win32]
+ '@rollup/rollup-win32-arm64-msvc@4.40.2':
+ resolution: {integrity: sha512-Bjv/HG8RRWLNkXwQQemdsWw4Mg+IJ29LK+bJPW2SCzPKOUaMmPEppQlu/Fqk1d7+DX3V7JbFdbkh/NMmurT6Pg==}
+ cpu: [arm64]
+ os: [win32]
+
'@rollup/rollup-win32-ia32-msvc@4.22.4':
resolution: {integrity: sha512-SiWG/1TuUdPvYmzmYnmd3IEifzR61Tragkbx9D3+R8mzQqDBz8v+BvZNDlkiTtI9T15KYZhP0ehn3Dld4n9J5g==}
cpu: [ia32]
os: [win32]
+ '@rollup/rollup-win32-ia32-msvc@4.40.2':
+ resolution: {integrity: sha512-dt1llVSGEsGKvzeIO76HToiYPNPYPkmjhMHhP00T9S4rDern8P2ZWvWAQUEJ+R1UdMWJ/42i/QqJ2WV765GZcA==}
+ cpu: [ia32]
+ os: [win32]
+
'@rollup/rollup-win32-x64-msvc@4.22.4':
resolution: {integrity: sha512-j8pPKp53/lq9lMXN57S8cFz0MynJk8OWNuUnXct/9KCpKU7DgU3bYMJhwWmcqC0UU29p8Lr0/7KEVcaM6bf47Q==}
cpu: [x64]
os: [win32]
+ '@rollup/rollup-win32-x64-msvc@4.40.2':
+ resolution: {integrity: sha512-bwspbWB04XJpeElvsp+DCylKfF4trJDa2Y9Go8O6A7YLX2LIKGcNK/CYImJN6ZP4DcuOHB4Utl3iCbnR62DudA==}
+ cpu: [x64]
+ os: [win32]
+
'@stylistic/eslint-plugin-js@1.8.0':
resolution: {integrity: sha512-jdvnzt+pZPg8TfclZlTZPiUbbima93ylvQ+wNgHLNmup3obY6heQvgewSu9i2CfS61BnRByv+F9fxQLPoNeHag==}
engines: {node: ^16.0.0 || >=18.0.0}
@@ -673,6 +779,9 @@ packages:
'@types/estree@1.0.6':
resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==}
+ '@types/estree@1.0.7':
+ resolution: {integrity: sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==}
+
'@types/json-schema@7.0.15':
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
@@ -707,6 +816,10 @@ packages:
resolution: {integrity: sha512-E0ntLvsfPqnPwng8b8y4OGuzh/iIOm2z8U3S9zic2TeMLW61u5IH2Q1wu0oSTkfrSzwbDJIB/Lm8O3//8BWMPA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ '@typescript-eslint/scope-manager@8.32.1':
+ resolution: {integrity: sha512-7IsIaIDeZn7kffk7qXC3o6Z4UblZJKV3UBpkvRNpr5NSyLji7tvTcvmnMNYuYLyh26mN8W723xpo3i4MlD33vA==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
'@typescript-eslint/type-utils@8.26.0':
resolution: {integrity: sha512-ruk0RNChLKz3zKGn2LwXuVoeBcUMh+jaqzN461uMMdxy5H9epZqIBtYj7UiPXRuOpaALXGbmRuZQhmwHhaS04Q==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@@ -718,12 +831,22 @@ packages:
resolution: {integrity: sha512-89B1eP3tnpr9A8L6PZlSjBvnJhWXtYfZhECqlBl1D9Lme9mHO6iWlsprBtVenQvY1HMhax1mWOjhtL3fh/u+pA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ '@typescript-eslint/types@8.32.1':
+ resolution: {integrity: sha512-YmybwXUJcgGqgAp6bEsgpPXEg6dcCyPyCSr0CAAueacR/CCBi25G3V8gGQ2kRzQRBNol7VQknxMs9HvVa9Rvfg==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
'@typescript-eslint/typescript-estree@8.26.0':
resolution: {integrity: sha512-tiJ1Hvy/V/oMVRTbEOIeemA2XoylimlDQ03CgPPNaHYZbpsc78Hmngnt+WXZfJX1pjQ711V7g0H7cSJThGYfPQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '>=4.8.4 <5.9.0'
+ '@typescript-eslint/typescript-estree@8.32.1':
+ resolution: {integrity: sha512-Y3AP9EIfYwBb4kWGb+simvPaqQoT5oJuzzj9m0i6FCY6SPvlomY2Ei4UEMm7+FXtlNJbor80ximyslzaQF6xhg==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ typescript: '>=4.8.4 <5.9.0'
+
'@typescript-eslint/utils@8.26.0':
resolution: {integrity: sha512-2L2tU3FVwhvU14LndnQCA2frYC8JnPDVKyQtWFPf8IYFMt/ykEN1bPolNhNbCVgOmdzTlWdusCTKA/9nKrf8Ig==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@@ -731,10 +854,21 @@ packages:
eslint: ^8.57.0 || ^9.0.0
typescript: '>=4.8.4 <5.9.0'
+ '@typescript-eslint/utils@8.32.1':
+ resolution: {integrity: sha512-DsSFNIgLSrc89gpq1LJB7Hm1YpuhK086DRDJSNrewcGvYloWW1vZLHBTIvarKZDcAORIy/uWNx8Gad+4oMpkSA==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ eslint: ^8.57.0 || ^9.0.0
+ typescript: '>=4.8.4 <5.9.0'
+
'@typescript-eslint/visitor-keys@8.26.0':
resolution: {integrity: sha512-2z8JQJWAzPdDd51dRQ/oqIJxe99/hoLIqmf8RMCAJQtYDc535W/Jt2+RTP4bP0aKeBG1F65yjIZuczOXCmbWwg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ '@typescript-eslint/visitor-keys@8.32.1':
+ resolution: {integrity: sha512-ar0tjQfObzhSaW3C3QNmTc5ofj0hDoNQ5XWrCy6zDyabdr0TWhCkClp+rywGNj/odAFBVzzJrK4tEq5M4Hmu4w==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
'@vitest/coverage-v8@2.0.5':
resolution: {integrity: sha512-qeFcySCg5FLO2bHHSa0tAZAOnAUbp4L6/A5JDuj9+bt53JREl8hpLjLHEWF0e/gWc8INVpJaqA7+Ene2rclpZg==}
peerDependencies:
@@ -779,6 +913,11 @@ packages:
engines: {node: '>=0.4.0'}
hasBin: true
+ acorn@8.14.1:
+ resolution: {integrity: sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==}
+ engines: {node: '>=0.4.0'}
+ hasBin: true
+
agent-base@7.1.1:
resolution: {integrity: sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==}
engines: {node: '>= 14'}
@@ -916,7 +1055,7 @@ packages:
resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==}
concat-map@0.0.1:
- resolution: {integrity: sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=}
+ resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
cross-spawn@5.1.0:
resolution: {integrity: sha512-pTgQJ5KC0d2hcY8eyL1IzlBPYjTkyH72XRZPnLyKus2mBfNjQs3klqbJU2VILqZryAZUt9JOb3h/mWMy23/f5A==}
@@ -1122,8 +1261,8 @@ packages:
resolution: {integrity: sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==}
engines: {node: '>=0.10'}
- esrap@1.4.3:
- resolution: {integrity: sha512-Xddc1RsoFJ4z9nR7W7BFaEPIp4UXoeQ0+077UdWLxbafMQFyU79sQJMk7kxNgRwQ9/aVgaKacCHC2pUACGwmYw==}
+ esrap@2.1.0:
+ resolution: {integrity: sha512-yzmPNpl7TBbMRC5Lj2JlJZNPml0tzqoqP5B1JXycNUwtqma9AKCO0M2wHrdgsHcy1WRW7S9rJknAMtByg3usgA==}
esrecurse@4.3.0:
resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==}
@@ -1161,6 +1300,10 @@ packages:
resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==}
engines: {node: '>=8.6.0'}
+ fast-glob@3.3.3:
+ resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==}
+ engines: {node: '>=8.6.0'}
+
fast-json-stable-stringify@2.1.0:
resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==}
@@ -1257,6 +1400,10 @@ packages:
resolution: {integrity: sha512-OkToC372DtlQeje9/zHIo5CT8lRP/FUgEOKBEhU4e0abL7J7CD24fD9ohiLN5hagG/kWCYj4K5oaxxtj2Z0Dig==}
engines: {node: '>=18'}
+ globals@15.15.0:
+ resolution: {integrity: sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==}
+ engines: {node: '>=18'}
+
globby@11.1.0:
resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==}
engines: {node: '>=10'}
@@ -1551,6 +1698,10 @@ packages:
resolution: {integrity: sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==}
engines: {node: '>=8.6'}
+ micromatch@4.0.8:
+ resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}
+ engines: {node: '>=8.6'}
+
mime-db@1.52.0:
resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
engines: {node: '>= 0.6'}
@@ -1559,10 +1710,6 @@ packages:
resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
engines: {node: '>= 0.6'}
- minimatch@10.0.1:
- resolution: {integrity: sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==}
- engines: {node: 20 || >=22}
-
minimatch@3.1.2:
resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
@@ -1585,8 +1732,8 @@ packages:
ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
- nanoid@3.3.8:
- resolution: {integrity: sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==}
+ nanoid@3.3.11:
+ resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
@@ -1751,8 +1898,8 @@ packages:
resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==}
engines: {node: '>=4'}
- postcss@8.5.1:
- resolution: {integrity: sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ==}
+ postcss@8.5.3:
+ resolution: {integrity: sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==}
engines: {node: ^10 || ^12 || >=14}
prelude-ls@1.2.1:
@@ -1827,6 +1974,11 @@ packages:
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
hasBin: true
+ rollup@4.40.2:
+ resolution: {integrity: sha512-tfUOg6DTP4rhQ3VjOO6B4wyrJnGOX85requAXvqYTHsOgb2TFJdZ3aWpT8W2kPoypSGP7dZUyzxJ9ee4buM5Fg==}
+ engines: {node: '>=18.0.0', npm: '>=8.0.0'}
+ hasBin: true
+
rrweb-cssom@0.7.1:
resolution: {integrity: sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==}
@@ -1861,6 +2013,11 @@ packages:
engines: {node: '>=10'}
hasBin: true
+ semver@7.7.2:
+ resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==}
+ engines: {node: '>=10'}
+ hasBin: true
+
serialize-javascript@6.0.2:
resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==}
@@ -2064,8 +2221,14 @@ packages:
peerDependencies:
typescript: '>=4.8.4'
- ts-declaration-location@1.0.5:
- resolution: {integrity: sha512-WqmlO9IoeYwCqJ2E9kHMcY9GZhhfLYItC3VnHDlPOrg6nNdUWS4wn4hhDZUPt60m1EvtjPIZyprTjpI992Bgzw==}
+ ts-api-utils@2.1.0:
+ resolution: {integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==}
+ engines: {node: '>=18.12'}
+ peerDependencies:
+ typescript: '>=4.8.4'
+
+ ts-declaration-location@1.0.7:
+ resolution: {integrity: sha512-EDyGAwH1gO0Ausm9gV6T2nUvBgXT5kGoCMJPllOaooZ+4VvJiKBdZE7wK18N1deEowhcUptS+5GXZK8U/fvpwA==}
peerDependencies:
typescript: '>=4.0.0'
@@ -2152,6 +2315,37 @@ packages:
terser:
optional: true
+ vite@5.4.19:
+ resolution: {integrity: sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA==}
+ engines: {node: ^18.0.0 || >=20.0.0}
+ hasBin: true
+ peerDependencies:
+ '@types/node': ^18.0.0 || >=20.0.0
+ less: '*'
+ lightningcss: ^1.21.0
+ sass: '*'
+ sass-embedded: '*'
+ stylus: '*'
+ sugarss: '*'
+ terser: ^5.4.0
+ peerDependenciesMeta:
+ '@types/node':
+ optional: true
+ less:
+ optional: true
+ lightningcss:
+ optional: true
+ sass:
+ optional: true
+ sass-embedded:
+ optional: true
+ stylus:
+ optional: true
+ sugarss:
+ optional: true
+ terser:
+ optional: true
+
vitefu@0.2.5:
resolution: {integrity: sha512-SgHtMLoqaeeGnd2evZ849ZbACbnwQCIwRH57t18FxcXoZop0uQu0uzlIhJBlF/eWVzuce0sHeqPcDo+evVcg8Q==}
peerDependencies:
@@ -2522,6 +2716,11 @@ snapshots:
eslint: 9.9.1
eslint-visitor-keys: 3.4.3
+ '@eslint-community/eslint-utils@4.7.0(eslint@9.9.1)':
+ dependencies:
+ eslint: 9.9.1
+ eslint-visitor-keys: 3.4.3
+
'@eslint-community/regexpp@4.12.1': {}
'@eslint/config-array@0.18.0':
@@ -2666,58 +2865,126 @@ snapshots:
optionalDependencies:
rollup: 4.22.4
+ '@rollup/pluginutils@5.1.0(rollup@4.40.2)':
+ dependencies:
+ '@types/estree': 1.0.6
+ estree-walker: 2.0.2
+ picomatch: 2.3.1
+ optionalDependencies:
+ rollup: 4.40.2
+
'@rollup/rollup-android-arm-eabi@4.22.4':
optional: true
+ '@rollup/rollup-android-arm-eabi@4.40.2':
+ optional: true
+
'@rollup/rollup-android-arm64@4.22.4':
optional: true
+ '@rollup/rollup-android-arm64@4.40.2':
+ optional: true
+
'@rollup/rollup-darwin-arm64@4.22.4':
optional: true
+ '@rollup/rollup-darwin-arm64@4.40.2':
+ optional: true
+
'@rollup/rollup-darwin-x64@4.22.4':
optional: true
+ '@rollup/rollup-darwin-x64@4.40.2':
+ optional: true
+
+ '@rollup/rollup-freebsd-arm64@4.40.2':
+ optional: true
+
+ '@rollup/rollup-freebsd-x64@4.40.2':
+ optional: true
+
'@rollup/rollup-linux-arm-gnueabihf@4.22.4':
optional: true
+ '@rollup/rollup-linux-arm-gnueabihf@4.40.2':
+ optional: true
+
'@rollup/rollup-linux-arm-musleabihf@4.22.4':
optional: true
+ '@rollup/rollup-linux-arm-musleabihf@4.40.2':
+ optional: true
+
'@rollup/rollup-linux-arm64-gnu@4.22.4':
optional: true
+ '@rollup/rollup-linux-arm64-gnu@4.40.2':
+ optional: true
+
'@rollup/rollup-linux-arm64-musl@4.22.4':
optional: true
+ '@rollup/rollup-linux-arm64-musl@4.40.2':
+ optional: true
+
+ '@rollup/rollup-linux-loongarch64-gnu@4.40.2':
+ optional: true
+
'@rollup/rollup-linux-powerpc64le-gnu@4.22.4':
optional: true
+ '@rollup/rollup-linux-powerpc64le-gnu@4.40.2':
+ optional: true
+
'@rollup/rollup-linux-riscv64-gnu@4.22.4':
optional: true
+ '@rollup/rollup-linux-riscv64-gnu@4.40.2':
+ optional: true
+
+ '@rollup/rollup-linux-riscv64-musl@4.40.2':
+ optional: true
+
'@rollup/rollup-linux-s390x-gnu@4.22.4':
optional: true
+ '@rollup/rollup-linux-s390x-gnu@4.40.2':
+ optional: true
+
'@rollup/rollup-linux-x64-gnu@4.22.4':
optional: true
+ '@rollup/rollup-linux-x64-gnu@4.40.2':
+ optional: true
+
'@rollup/rollup-linux-x64-musl@4.22.4':
optional: true
+ '@rollup/rollup-linux-x64-musl@4.40.2':
+ optional: true
+
'@rollup/rollup-win32-arm64-msvc@4.22.4':
optional: true
+ '@rollup/rollup-win32-arm64-msvc@4.40.2':
+ optional: true
+
'@rollup/rollup-win32-ia32-msvc@4.22.4':
optional: true
+ '@rollup/rollup-win32-ia32-msvc@4.40.2':
+ optional: true
+
'@rollup/rollup-win32-x64-msvc@4.22.4':
optional: true
+ '@rollup/rollup-win32-x64-msvc@4.40.2':
+ optional: true
+
'@stylistic/eslint-plugin-js@1.8.0(eslint@9.9.1)':
dependencies:
'@types/eslint': 8.56.12
- acorn: 8.14.0
+ acorn: 8.14.1
escape-string-regexp: 4.0.0
eslint: 9.9.1
eslint-visitor-keys: 3.4.3
@@ -2738,25 +3005,25 @@ snapshots:
typescript: 5.5.4
typescript-eslint: 8.26.0(eslint@9.9.1)(typescript@5.5.4)
- '@sveltejs/vite-plugin-svelte-inspector@3.0.0-next.2(@sveltejs/vite-plugin-svelte@4.0.0-next.6(svelte@packages+svelte)(vite@5.4.14(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)))(svelte@packages+svelte)(vite@5.4.14(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0))':
+ '@sveltejs/vite-plugin-svelte-inspector@3.0.0-next.2(@sveltejs/vite-plugin-svelte@4.0.0-next.6(svelte@packages+svelte)(vite@5.4.19(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)))(svelte@packages+svelte)(vite@5.4.19(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0))':
dependencies:
- '@sveltejs/vite-plugin-svelte': 4.0.0-next.6(svelte@packages+svelte)(vite@5.4.14(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0))
+ '@sveltejs/vite-plugin-svelte': 4.0.0-next.6(svelte@packages+svelte)(vite@5.4.19(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0))
debug: 4.4.0
svelte: link:packages/svelte
- vite: 5.4.14(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)
+ vite: 5.4.19(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)
transitivePeerDependencies:
- supports-color
- '@sveltejs/vite-plugin-svelte@4.0.0-next.6(svelte@packages+svelte)(vite@5.4.14(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0))':
+ '@sveltejs/vite-plugin-svelte@4.0.0-next.6(svelte@packages+svelte)(vite@5.4.19(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0))':
dependencies:
- '@sveltejs/vite-plugin-svelte-inspector': 3.0.0-next.2(@sveltejs/vite-plugin-svelte@4.0.0-next.6(svelte@packages+svelte)(vite@5.4.14(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)))(svelte@packages+svelte)(vite@5.4.14(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0))
+ '@sveltejs/vite-plugin-svelte-inspector': 3.0.0-next.2(@sveltejs/vite-plugin-svelte@4.0.0-next.6(svelte@packages+svelte)(vite@5.4.19(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)))(svelte@packages+svelte)(vite@5.4.19(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0))
debug: 4.4.0
deepmerge: 4.3.1
kleur: 4.1.5
magic-string: 0.30.17
svelte: link:packages/svelte
- vite: 5.4.14(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)
- vitefu: 0.2.5(vite@5.4.14(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0))
+ vite: 5.4.19(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)
+ vitefu: 0.2.5(vite@5.4.19(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0))
transitivePeerDependencies:
- supports-color
@@ -2771,13 +3038,15 @@ snapshots:
'@types/eslint@8.56.12':
dependencies:
- '@types/estree': 1.0.6
+ '@types/estree': 1.0.7
'@types/json-schema': 7.0.15
'@types/estree@1.0.5': {}
'@types/estree@1.0.6': {}
+ '@types/estree@1.0.7': {}
+
'@types/json-schema@7.0.15': {}
'@types/node@12.20.55': {}
@@ -2824,6 +3093,11 @@ snapshots:
'@typescript-eslint/types': 8.26.0
'@typescript-eslint/visitor-keys': 8.26.0
+ '@typescript-eslint/scope-manager@8.32.1':
+ dependencies:
+ '@typescript-eslint/types': 8.32.1
+ '@typescript-eslint/visitor-keys': 8.32.1
+
'@typescript-eslint/type-utils@8.26.0(eslint@9.9.1)(typescript@5.5.4)':
dependencies:
'@typescript-eslint/typescript-estree': 8.26.0(typescript@5.5.4)
@@ -2837,6 +3111,8 @@ snapshots:
'@typescript-eslint/types@8.26.0': {}
+ '@typescript-eslint/types@8.32.1': {}
+
'@typescript-eslint/typescript-estree@8.26.0(typescript@5.5.4)':
dependencies:
'@typescript-eslint/types': 8.26.0
@@ -2851,6 +3127,20 @@ snapshots:
transitivePeerDependencies:
- supports-color
+ '@typescript-eslint/typescript-estree@8.32.1(typescript@5.5.4)':
+ dependencies:
+ '@typescript-eslint/types': 8.32.1
+ '@typescript-eslint/visitor-keys': 8.32.1
+ debug: 4.4.0
+ fast-glob: 3.3.3
+ is-glob: 4.0.3
+ minimatch: 9.0.5
+ semver: 7.7.2
+ ts-api-utils: 2.1.0(typescript@5.5.4)
+ typescript: 5.5.4
+ transitivePeerDependencies:
+ - supports-color
+
'@typescript-eslint/utils@8.26.0(eslint@9.9.1)(typescript@5.5.4)':
dependencies:
'@eslint-community/eslint-utils': 4.4.1(eslint@9.9.1)
@@ -2862,11 +3152,27 @@ snapshots:
transitivePeerDependencies:
- supports-color
+ '@typescript-eslint/utils@8.32.1(eslint@9.9.1)(typescript@5.5.4)':
+ dependencies:
+ '@eslint-community/eslint-utils': 4.7.0(eslint@9.9.1)
+ '@typescript-eslint/scope-manager': 8.32.1
+ '@typescript-eslint/types': 8.32.1
+ '@typescript-eslint/typescript-estree': 8.32.1(typescript@5.5.4)
+ eslint: 9.9.1
+ typescript: 5.5.4
+ transitivePeerDependencies:
+ - supports-color
+
'@typescript-eslint/visitor-keys@8.26.0':
dependencies:
'@typescript-eslint/types': 8.26.0
eslint-visitor-keys: 4.2.0
+ '@typescript-eslint/visitor-keys@8.32.1':
+ dependencies:
+ '@typescript-eslint/types': 8.32.1
+ eslint-visitor-keys: 4.2.0
+
'@vitest/coverage-v8@2.0.5(vitest@2.1.9(@types/node@20.12.7)(jsdom@25.0.1)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0))':
dependencies:
'@ampproject/remapping': 2.3.0
@@ -2929,8 +3235,14 @@ snapshots:
dependencies:
acorn: 8.14.0
+ acorn-jsx@5.3.2(acorn@8.14.1):
+ dependencies:
+ acorn: 8.14.1
+
acorn@8.14.0: {}
+ acorn@8.14.1: {}
+
agent-base@7.1.1:
dependencies:
debug: 4.4.0
@@ -3184,7 +3496,7 @@ snapshots:
eslint-compat-utils@0.5.1(eslint@9.9.1):
dependencies:
eslint: 9.9.1
- semver: 7.7.1
+ semver: 7.7.2
eslint-config-prettier@9.1.0(eslint@9.9.1):
dependencies:
@@ -3192,7 +3504,7 @@ snapshots:
eslint-plugin-es-x@7.8.0(eslint@9.9.1):
dependencies:
- '@eslint-community/eslint-utils': 4.4.1(eslint@9.9.1)
+ '@eslint-community/eslint-utils': 4.7.0(eslint@9.9.1)
'@eslint-community/regexpp': 4.12.1
eslint: 9.9.1
eslint-compat-utils: 0.5.1(eslint@9.9.1)
@@ -3201,35 +3513,35 @@ snapshots:
eslint-plugin-n@17.16.1(eslint@9.9.1)(typescript@5.5.4):
dependencies:
- '@eslint-community/eslint-utils': 4.4.1(eslint@9.9.1)
- '@typescript-eslint/utils': 8.26.0(eslint@9.9.1)(typescript@5.5.4)
+ '@eslint-community/eslint-utils': 4.7.0(eslint@9.9.1)
+ '@typescript-eslint/utils': 8.32.1(eslint@9.9.1)(typescript@5.5.4)
enhanced-resolve: 5.18.1
eslint: 9.9.1
eslint-plugin-es-x: 7.8.0(eslint@9.9.1)
get-tsconfig: 4.10.0
- globals: 15.14.0
+ globals: 15.15.0
ignore: 5.3.2
minimatch: 9.0.5
- semver: 7.7.1
- ts-declaration-location: 1.0.5(typescript@5.5.4)
+ semver: 7.7.2
+ ts-declaration-location: 1.0.7(typescript@5.5.4)
transitivePeerDependencies:
- supports-color
- typescript
eslint-plugin-svelte@2.38.0(eslint@9.9.1)(svelte@packages+svelte):
dependencies:
- '@eslint-community/eslint-utils': 4.4.1(eslint@9.9.1)
+ '@eslint-community/eslint-utils': 4.7.0(eslint@9.9.1)
'@jridgewell/sourcemap-codec': 1.5.0
debug: 4.4.0
eslint: 9.9.1
eslint-compat-utils: 0.5.1(eslint@9.9.1)
esutils: 2.0.3
known-css-properties: 0.30.0
- postcss: 8.5.1
- postcss-load-config: 3.1.4(postcss@8.5.1)
- postcss-safe-parser: 6.0.0(postcss@8.5.1)
+ postcss: 8.5.3
+ postcss-load-config: 3.1.4(postcss@8.5.3)
+ postcss-safe-parser: 6.0.0(postcss@8.5.3)
postcss-selector-parser: 6.1.2
- semver: 7.7.1
+ semver: 7.7.2
svelte-eslint-parser: 0.43.0(svelte@packages+svelte)
optionalDependencies:
svelte: link:packages/svelte
@@ -3300,8 +3612,8 @@ snapshots:
espree@9.6.1:
dependencies:
- acorn: 8.14.0
- acorn-jsx: 5.3.2(acorn@8.14.0)
+ acorn: 8.14.1
+ acorn-jsx: 5.3.2(acorn@8.14.1)
eslint-visitor-keys: 3.4.3
esprima@4.0.1: {}
@@ -3310,7 +3622,7 @@ snapshots:
dependencies:
estraverse: 5.3.0
- esrap@1.4.3:
+ esrap@2.1.0:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.0
@@ -3348,6 +3660,14 @@ snapshots:
merge2: 1.4.1
micromatch: 4.0.5
+ fast-glob@3.3.3:
+ dependencies:
+ '@nodelib/fs.stat': 2.0.5
+ '@nodelib/fs.walk': 1.2.8
+ glob-parent: 5.1.2
+ merge2: 1.4.1
+ micromatch: 4.0.8
+
fast-json-stable-stringify@2.1.0: {}
fast-levenshtein@2.0.6: {}
@@ -3447,6 +3767,8 @@ snapshots:
globals@15.14.0: {}
+ globals@15.15.0: {}
+
globby@11.1.0:
dependencies:
array-union: 2.1.0
@@ -3740,16 +4062,17 @@ snapshots:
braces: 3.0.3
picomatch: 2.3.1
+ micromatch@4.0.8:
+ dependencies:
+ braces: 3.0.3
+ picomatch: 2.3.1
+
mime-db@1.52.0: {}
mime-types@2.1.35:
dependencies:
mime-db: 1.52.0
- minimatch@10.0.1:
- dependencies:
- brace-expansion: 2.0.1
-
minimatch@3.1.2:
dependencies:
brace-expansion: 1.1.11
@@ -3766,7 +4089,7 @@ snapshots:
ms@2.1.3: {}
- nanoid@3.3.8: {}
+ nanoid@3.3.11: {}
natural-compare@1.4.0: {}
@@ -3875,29 +4198,29 @@ snapshots:
'@polka/url': 1.0.0-next.25
trouter: 4.0.0
- postcss-load-config@3.1.4(postcss@8.5.1):
+ postcss-load-config@3.1.4(postcss@8.5.3):
dependencies:
lilconfig: 2.1.0
yaml: 1.10.2
optionalDependencies:
- postcss: 8.5.1
+ postcss: 8.5.3
- postcss-safe-parser@6.0.0(postcss@8.5.1):
+ postcss-safe-parser@6.0.0(postcss@8.5.3):
dependencies:
- postcss: 8.5.1
+ postcss: 8.5.3
- postcss-scss@4.0.9(postcss@8.5.1):
+ postcss-scss@4.0.9(postcss@8.5.3):
dependencies:
- postcss: 8.5.1
+ postcss: 8.5.3
postcss-selector-parser@6.1.2:
dependencies:
cssesc: 3.0.0
util-deprecate: 1.0.2
- postcss@8.5.1:
+ postcss@8.5.3:
dependencies:
- nanoid: 3.3.8
+ nanoid: 3.3.11
picocolors: 1.1.1
source-map-js: 1.2.1
@@ -3974,6 +4297,32 @@ snapshots:
'@rollup/rollup-win32-x64-msvc': 4.22.4
fsevents: 2.3.3
+ rollup@4.40.2:
+ dependencies:
+ '@types/estree': 1.0.7
+ optionalDependencies:
+ '@rollup/rollup-android-arm-eabi': 4.40.2
+ '@rollup/rollup-android-arm64': 4.40.2
+ '@rollup/rollup-darwin-arm64': 4.40.2
+ '@rollup/rollup-darwin-x64': 4.40.2
+ '@rollup/rollup-freebsd-arm64': 4.40.2
+ '@rollup/rollup-freebsd-x64': 4.40.2
+ '@rollup/rollup-linux-arm-gnueabihf': 4.40.2
+ '@rollup/rollup-linux-arm-musleabihf': 4.40.2
+ '@rollup/rollup-linux-arm64-gnu': 4.40.2
+ '@rollup/rollup-linux-arm64-musl': 4.40.2
+ '@rollup/rollup-linux-loongarch64-gnu': 4.40.2
+ '@rollup/rollup-linux-powerpc64le-gnu': 4.40.2
+ '@rollup/rollup-linux-riscv64-gnu': 4.40.2
+ '@rollup/rollup-linux-riscv64-musl': 4.40.2
+ '@rollup/rollup-linux-s390x-gnu': 4.40.2
+ '@rollup/rollup-linux-x64-gnu': 4.40.2
+ '@rollup/rollup-linux-x64-musl': 4.40.2
+ '@rollup/rollup-win32-arm64-msvc': 4.40.2
+ '@rollup/rollup-win32-ia32-msvc': 4.40.2
+ '@rollup/rollup-win32-x64-msvc': 4.40.2
+ fsevents: 2.3.3
+
rrweb-cssom@0.7.1: {}
run-applescript@7.0.0: {}
@@ -4003,6 +4352,8 @@ snapshots:
semver@7.7.1: {}
+ semver@7.7.2: {}
+
serialize-javascript@6.0.2:
dependencies:
randombytes: 2.1.0
@@ -4092,8 +4443,8 @@ snapshots:
eslint-scope: 7.2.2
eslint-visitor-keys: 3.4.3
espree: 9.6.1
- postcss: 8.5.1
- postcss-scss: 4.0.9(postcss@8.5.1)
+ postcss: 8.5.3
+ postcss-scss: 4.0.9(postcss@8.5.3)
optionalDependencies:
svelte: link:packages/svelte
@@ -4173,9 +4524,13 @@ snapshots:
dependencies:
typescript: 5.5.4
- ts-declaration-location@1.0.5(typescript@5.5.4):
+ ts-api-utils@2.1.0(typescript@5.5.4):
dependencies:
- minimatch: 10.0.1
+ typescript: 5.5.4
+
+ ts-declaration-location@1.0.7(typescript@5.5.4):
+ dependencies:
+ picomatch: 4.0.2
typescript: 5.5.4
type-check@0.4.0:
@@ -4214,7 +4569,7 @@ snapshots:
debug: 4.4.0
es-module-lexer: 1.6.0
pathe: 1.1.2
- vite: 5.4.14(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)
+ vite: 5.4.19(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)
transitivePeerDependencies:
- '@types/node'
- less
@@ -4226,10 +4581,10 @@ snapshots:
- supports-color
- terser
- vite-plugin-inspect@0.8.4(rollup@4.22.4)(vite@5.4.14(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)):
+ vite-plugin-inspect@0.8.4(rollup@4.40.2)(vite@5.4.19(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)):
dependencies:
'@antfu/utils': 0.7.8
- '@rollup/pluginutils': 5.1.0(rollup@4.22.4)
+ '@rollup/pluginutils': 5.1.0(rollup@4.40.2)
debug: 4.4.0
error-stack-parser-es: 0.1.1
fs-extra: 11.2.0
@@ -4237,7 +4592,7 @@ snapshots:
perfect-debounce: 1.0.0
picocolors: 1.1.1
sirv: 2.0.4
- vite: 5.4.14(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)
+ vite: 5.4.19(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)
transitivePeerDependencies:
- rollup
- supports-color
@@ -4245,7 +4600,7 @@ snapshots:
vite@5.4.14(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0):
dependencies:
esbuild: 0.21.5
- postcss: 8.5.1
+ postcss: 8.5.3
rollup: 4.22.4
optionalDependencies:
'@types/node': 20.12.7
@@ -4254,9 +4609,21 @@ snapshots:
sass: 1.70.0
terser: 5.27.0
- vitefu@0.2.5(vite@5.4.14(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)):
+ vite@5.4.19(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0):
+ dependencies:
+ esbuild: 0.21.5
+ postcss: 8.5.3
+ rollup: 4.40.2
optionalDependencies:
- vite: 5.4.14(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)
+ '@types/node': 20.12.7
+ fsevents: 2.3.3
+ lightningcss: 1.23.0
+ sass: 1.70.0
+ terser: 5.27.0
+
+ vitefu@0.2.5(vite@5.4.19(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)):
+ optionalDependencies:
+ vite: 5.4.19(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)
vitest@2.1.9(@types/node@20.12.7)(jsdom@25.0.1)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0):
dependencies: