Merge branch 'main' into form-reset-listener

pull/10617/head
Simon Holthausen 2 years ago
commit a825d457ab

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: improve bind:this support around proxyied state

@ -0,0 +1,5 @@
---
"svelte": patch
---
feat: add reactive Set class to svelte/reactivity

@ -0,0 +1,5 @@
---
"svelte": patch
---
perf: bail early when traversing non-state

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: better ownership mutation validation

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: bump specificity on all members of a selector list

@ -0,0 +1,5 @@
---
"svelte": patch
---
breaking: preserve slots inside templates with a shadowrootmode attribute

@ -0,0 +1,5 @@
---
'svelte': patch
---
breaking: make `$props()` rune non-generic

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: improve namespace inference when having `{@render}` and `{@html}` tags

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: don't collapse whitespace within text nodes

@ -0,0 +1,5 @@
---
"svelte": patch
---
feat: improve ssr html mismatch validation

@ -2,4 +2,4 @@
'svelte': patch 'svelte': patch
--- ---
fix: port over props that were set prior to initialization fix: port over props that were set prior to initialisation

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: invalidate store when mutated inside each block

@ -0,0 +1,5 @@
---
"svelte": patch
---
chore: custom elements validation

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: improve event delegation with shadowed bindings

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: prevent reactive statement reruns when they have indirect cyclic dependencies

@ -0,0 +1,5 @@
---
"svelte": patch
---
feat: add reactive Date object to svelte/reactivity

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: improve ssr output of dynamic textarea elements

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: make `set.has(...)` granular for existing properties'

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: ensure performance.now() and requestAnimationFrame() are polyfilled in ssr

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: handle TypeScript's optional parameter syntax in snippets

@ -20,6 +20,8 @@
"big-eggs-flash", "big-eggs-flash",
"big-eyes-carry", "big-eyes-carry",
"big-geese-act", "big-geese-act",
"blue-ants-raise",
"blue-rules-juggle",
"blue-timers-film", "blue-timers-film",
"brave-points-sleep", "brave-points-sleep",
"brave-shrimps-kiss", "brave-shrimps-kiss",
@ -29,7 +31,10 @@
"bright-snakes-sing", "bright-snakes-sing",
"brown-months-fry", "brown-months-fry",
"brown-spoons-boil", "brown-spoons-boil",
"calm-ravens-sneeze",
"chatty-beans-divide",
"chatty-cups-drop", "chatty-cups-drop",
"chatty-sloths-allow",
"chatty-taxis-juggle", "chatty-taxis-juggle",
"chilled-pumas-invite", "chilled-pumas-invite",
"chilly-dolphins-lick", "chilly-dolphins-lick",
@ -38,11 +43,13 @@
"clever-chefs-relate", "clever-chefs-relate",
"clever-rockets-burn", "clever-rockets-burn",
"cold-birds-own", "cold-birds-own",
"cold-masks-learn",
"cool-ants-leave", "cool-ants-leave",
"cool-rabbits-tickle", "cool-rabbits-tickle",
"cool-roses-trade", "cool-roses-trade",
"cuddly-pianos-drop", "cuddly-pianos-drop",
"curly-lizards-dream", "curly-lizards-dream",
"curvy-buses-laugh",
"curvy-cups-cough", "curvy-cups-cough",
"curvy-ties-shout", "curvy-ties-shout",
"cyan-flowers-destroy", "cyan-flowers-destroy",
@ -54,18 +61,21 @@
"dry-clocks-grow", "dry-clocks-grow",
"dry-eggs-play", "dry-eggs-play",
"dry-eggs-retire", "dry-eggs-retire",
"dull-coins-vanish",
"dull-mangos-wave", "dull-mangos-wave",
"dull-roses-relate", "dull-roses-relate",
"early-ads-tie", "early-ads-tie",
"eight-steaks-shout", "eight-steaks-shout",
"eighty-bikes-camp", "eighty-bikes-camp",
"eighty-days-cheat", "eighty-days-cheat",
"eleven-cycles-applaud",
"empty-bags-heal", "empty-bags-heal",
"empty-bulldogs-exercise", "empty-bulldogs-exercise",
"empty-crabs-think", "empty-crabs-think",
"empty-tools-share", "empty-tools-share",
"fair-crabs-check", "fair-crabs-check",
"fair-pianos-talk", "fair-pianos-talk",
"fair-spies-repeat",
"famous-falcons-melt", "famous-falcons-melt",
"famous-knives-sneeze", "famous-knives-sneeze",
"famous-pants-pay", "famous-pants-pay",
@ -83,6 +93,7 @@
"forty-peaches-unite", "forty-peaches-unite",
"forty-suns-smile", "forty-suns-smile",
"four-flies-hammer", "four-flies-hammer",
"fresh-impalas-bow",
"fresh-weeks-trade", "fresh-weeks-trade",
"friendly-candles-relate", "friendly-candles-relate",
"friendly-lies-camp", "friendly-lies-camp",
@ -91,7 +102,9 @@
"gentle-sheep-hug", "gentle-sheep-hug",
"gentle-spies-happen", "gentle-spies-happen",
"giant-moons-own", "giant-moons-own",
"giant-planets-shake",
"giant-roses-press", "giant-roses-press",
"good-buses-reply",
"good-cars-visit", "good-cars-visit",
"good-pianos-jump", "good-pianos-jump",
"good-rivers-yawn", "good-rivers-yawn",
@ -101,6 +114,7 @@
"green-eggs-approve", "green-eggs-approve",
"green-hounds-play", "green-hounds-play",
"green-tigers-judge", "green-tigers-judge",
"happy-beds-scream",
"happy-suits-film", "happy-suits-film",
"healthy-planes-vanish", "healthy-planes-vanish",
"heavy-comics-move", "heavy-comics-move",
@ -111,6 +125,7 @@
"honest-icons-change", "honest-icons-change",
"hungry-boxes-relate", "hungry-boxes-relate",
"hungry-dots-fry", "hungry-dots-fry",
"hungry-singers-share",
"hungry-tips-unite", "hungry-tips-unite",
"hungry-trees-travel", "hungry-trees-travel",
"itchy-beans-melt", "itchy-beans-melt",
@ -118,6 +133,7 @@
"itchy-lions-wash", "itchy-lions-wash",
"itchy-terms-guess", "itchy-terms-guess",
"khaki-cooks-develop", "khaki-cooks-develop",
"khaki-ligers-sing",
"khaki-mails-draw", "khaki-mails-draw",
"khaki-moose-arrive", "khaki-moose-arrive",
"kind-baboons-approve", "kind-baboons-approve",
@ -125,6 +141,7 @@
"kind-dots-sort", "kind-dots-sort",
"kind-eagles-join", "kind-eagles-join",
"kind-rings-flash", "kind-rings-flash",
"kind-spoons-return",
"large-clouds-carry", "large-clouds-carry",
"large-turkeys-deny", "large-turkeys-deny",
"late-crabs-lay", "late-crabs-lay",
@ -136,6 +153,7 @@
"light-days-clean", "light-days-clean",
"light-humans-hang", "light-humans-hang",
"light-pens-watch", "light-pens-watch",
"little-pans-jog",
"long-buckets-lay", "long-buckets-lay",
"long-crews-return", "long-crews-return",
"long-lobsters-mate", "long-lobsters-mate",
@ -150,13 +168,17 @@
"mighty-files-hammer", "mighty-files-hammer",
"moody-carrots-lay", "moody-carrots-lay",
"moody-frogs-exist", "moody-frogs-exist",
"moody-houses-argue",
"moody-owls-cry", "moody-owls-cry",
"moody-sheep-type",
"nasty-lions-double", "nasty-lions-double",
"nasty-yaks-peel", "nasty-yaks-peel",
"neat-boats-shake", "neat-boats-shake",
"neat-dingos-clap", "neat-dingos-clap",
"nervous-spoons-relax", "nervous-spoons-relax",
"new-boats-wait", "new-boats-wait",
"new-rabbits-flow",
"nice-avocados-move",
"ninety-dingos-walk", "ninety-dingos-walk",
"odd-buckets-lie", "odd-buckets-lie",
"odd-needles-joke", "odd-needles-joke",
@ -174,16 +196,20 @@
"olive-socks-kick", "olive-socks-kick",
"orange-crews-rescue", "orange-crews-rescue",
"orange-dingos-poke", "orange-dingos-poke",
"orange-yaks-protect",
"pink-bikes-agree",
"pink-mayflies-tie", "pink-mayflies-tie",
"polite-dolphins-care", "polite-dolphins-care",
"polite-pumpkins-guess", "polite-pumpkins-guess",
"polite-ravens-study", "polite-ravens-study",
"poor-eggs-enjoy", "poor-eggs-enjoy",
"poor-seahorses-flash", "poor-seahorses-flash",
"popular-ligers-perform",
"popular-mangos-rest", "popular-mangos-rest",
"pretty-ties-help", "pretty-ties-help",
"purple-dragons-peel", "purple-dragons-peel",
"quiet-apricots-dream", "quiet-apricots-dream",
"quiet-berries-end",
"quiet-camels-mate", "quiet-camels-mate",
"quiet-crabs-nail", "quiet-crabs-nail",
"quiet-timers-speak", "quiet-timers-speak",
@ -202,6 +228,9 @@
"rotten-bags-type", "rotten-bags-type",
"rotten-buckets-develop", "rotten-buckets-develop",
"rotten-experts-relax", "rotten-experts-relax",
"rotten-poems-applaud",
"rotten-rules-invite",
"rude-ghosts-tickle",
"selfish-dragons-knock", "selfish-dragons-knock",
"selfish-tools-hide", "selfish-tools-hide",
"serious-kids-deliver", "serious-kids-deliver",
@ -210,6 +239,7 @@
"seven-deers-jam", "seven-deers-jam",
"seven-hornets-smile", "seven-hornets-smile",
"seven-jobs-sniff", "seven-jobs-sniff",
"seven-masks-end",
"seven-ravens-check", "seven-ravens-check",
"shaggy-cameras-live", "shaggy-cameras-live",
"sharp-gorillas-impress", "sharp-gorillas-impress",
@ -218,6 +248,7 @@
"shiny-baboons-play", "shiny-baboons-play",
"shiny-shrimps-march", "shiny-shrimps-march",
"short-buses-camp", "short-buses-camp",
"short-countries-rush",
"silent-apes-report", "silent-apes-report",
"silly-laws-happen", "silly-laws-happen",
"silver-points-approve", "silver-points-approve",
@ -227,10 +258,13 @@
"slimy-walls-draw", "slimy-walls-draw",
"slow-beds-shave", "slow-beds-shave",
"slow-chefs-dream", "slow-chefs-dream",
"slow-kids-sparkle",
"slow-wombats-reply", "slow-wombats-reply",
"small-papayas-laugh", "small-papayas-laugh",
"small-sheep-type",
"smart-parents-swim", "smart-parents-swim",
"smart-zebras-pay", "smart-zebras-pay",
"smooth-rings-rush",
"soft-clocks-remember", "soft-clocks-remember",
"soft-geese-learn", "soft-geese-learn",
"soft-months-grab", "soft-months-grab",
@ -241,6 +275,7 @@
"sour-weeks-fix", "sour-weeks-fix",
"spicy-jeans-deliver", "spicy-jeans-deliver",
"spicy-plums-admire", "spicy-plums-admire",
"spotty-houses-search",
"spotty-pens-agree", "spotty-pens-agree",
"spotty-rocks-destroy", "spotty-rocks-destroy",
"spotty-spiders-compare", "spotty-spiders-compare",
@ -266,8 +301,11 @@
"tall-tigers-wait", "tall-tigers-wait",
"tame-cycles-kneel", "tame-cycles-kneel",
"tame-spies-drum", "tame-spies-drum",
"tasty-cheetahs-appear",
"tasty-numbers-perform", "tasty-numbers-perform",
"tasty-steaks-smile",
"ten-foxes-repeat", "ten-foxes-repeat",
"ten-jokes-divide",
"ten-peaches-sleep", "ten-peaches-sleep",
"ten-ties-repair", "ten-ties-repair",
"ten-worms-reflect", "ten-worms-reflect",
@ -287,25 +325,30 @@
"tidy-buses-whisper", "tidy-buses-whisper",
"tidy-starfishes-allow", "tidy-starfishes-allow",
"tiny-kings-whisper", "tiny-kings-whisper",
"tough-radios-punch",
"twelve-dragons-join", "twelve-dragons-join",
"twelve-onions-juggle", "twelve-onions-juggle",
"twelve-worms-jog", "twelve-worms-jog",
"two-dragons-yell", "two-dragons-yell",
"two-falcons-buy", "two-falcons-buy",
"unlucky-boxes-obey", "unlucky-boxes-obey",
"unlucky-steaks-warn",
"unlucky-trees-lick", "unlucky-trees-lick",
"violet-pigs-jam", "violet-pigs-jam",
"weak-terms-destroy", "weak-terms-destroy",
"wet-games-fly", "wet-games-fly",
"wet-wombats-repeat",
"wicked-clouds-exercise", "wicked-clouds-exercise",
"wicked-doors-train", "wicked-doors-train",
"wicked-hairs-cheer", "wicked-hairs-cheer",
"wild-foxes-wonder", "wild-foxes-wonder",
"wise-apples-care",
"wise-dancers-hang", "wise-dancers-hang",
"wise-donkeys-marry", "wise-donkeys-marry",
"wise-jobs-admire", "wise-jobs-admire",
"wise-radios-exercise", "wise-radios-exercise",
"witty-camels-warn", "witty-camels-warn",
"witty-readers-provide",
"witty-steaks-dream", "witty-steaks-dream",
"witty-tomatoes-care", "witty-tomatoes-care",
"witty-years-crash", "witty-years-crash",

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: improve ssr code generation for class property $derived

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: use getters for derived class state fields, with memoisation

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: adjust keyed each block equality handling

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: eagerly unsubscribe when store is changed

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: warn when `$props` rune not called

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: error when exporting reassigned state from module context

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: improve internal render effect sequencing

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: improve indexed each equality

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: improve derived rune destructuring support

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: adjust render effect ordering

@ -0,0 +1,5 @@
---
"svelte": patch
---
feat: allow arbitrary call expressions and optional chaining for snippets

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: add `$set` and `$on` methods in legacy compat mode

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: deduplicate generated props and action arg names

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: prevent snippet children conflict

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: improve deep_read performance

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: handle component binding mutation

@ -212,7 +212,7 @@ If a statement consists entirely of an assignment to an undeclared variable, Sve
A _store_ is an object that allows reactive access to a value via a simple _store contract_. The [`svelte/store` module](/docs/svelte-store) contains minimal store implementations which fulfil this contract. A _store_ is an object that allows reactive access to a value via a simple _store contract_. The [`svelte/store` module](/docs/svelte-store) contains minimal store implementations which fulfil this contract.
Any time you have a reference to a store, you can access its value inside a component by prefixing it with the `$` character. This causes Svelte to declare the prefixed variable, subscribe to the store at component initialization and unsubscribe when appropriate. Any time you have a reference to a store, you can access its value inside a component by prefixing it with the `$` character. This causes Svelte to declare the prefixed variable, subscribe to the store at component initialisation and unsubscribe when appropriate.
Assignments to `$`-prefixed variables require that the variable be a writable store, and will result in a call to the store's `.set` method. Assignments to `$`-prefixed variables require that the variable be a writable store, and will result in a call to the store's `.set` method.
@ -321,7 +321,7 @@ To apply styles to a selector globally, use the `:global(...)` modifier.
If you want to make @keyframes that are accessible globally, you need to prepend your keyframe names with `-global-`. If you want to make @keyframes that are accessible globally, you need to prepend your keyframe names with `-global-`.
The `-global-` part will be removed when compiled, and the keyframe then be referenced using just `my-animation-name` elsewhere in your code. The `-global-` part will be removed when compiled, and the keyframe will then be referenced using just `my-animation-name` elsewhere in your code.
```svelte ```svelte
<style> <style>
@ -333,9 +333,9 @@ The `-global-` part will be removed when compiled, and the keyframe then be refe
There should only be 1 top-level `<style>` tag per component. There should only be 1 top-level `<style>` tag per component.
However, it is possible to have `<style>` tag nested inside other elements or logic blocks. However, it is possible to have a `<style>` tag nested inside other elements or logic blocks.
In that case, the `<style>` tag will be inserted as-is into the DOM, no scoping or processing will be done on the `<style>` tag. In that case, the `<style>` tag will be inserted as-is into the DOM; no scoping or processing will be done on the `<style>` tag.
```svelte ```svelte
<div> <div>

@ -1,5 +1,143 @@
# svelte # svelte
## 5.0.0-next.78
### Patch Changes
- fix: invalidate store when mutated inside each block ([#10785](https://github.com/sveltejs/svelte/pull/10785))
- fix: make `set.has(...)` granular for existing properties' ([#10793](https://github.com/sveltejs/svelte/pull/10793))
## 5.0.0-next.77
### Patch Changes
- fix: adjust render effect ordering ([#10783](https://github.com/sveltejs/svelte/pull/10783))
- fix: handle component binding mutation ([#10786](https://github.com/sveltejs/svelte/pull/10786))
## 5.0.0-next.76
### Patch Changes
- feat: add reactive Set class to svelte/reactivity ([#10781](https://github.com/sveltejs/svelte/pull/10781))
- breaking: make `$props()` rune non-generic ([#10694](https://github.com/sveltejs/svelte/pull/10694))
- fix: improve internal render effect sequencing ([#10769](https://github.com/sveltejs/svelte/pull/10769))
## 5.0.0-next.75
### Patch Changes
- fix: use getters for derived class state fields, with memoisation ([#10757](https://github.com/sveltejs/svelte/pull/10757))
## 5.0.0-next.74
### Patch Changes
- fix: prevent reactive statement reruns when they have indirect cyclic dependencies ([#10736](https://github.com/sveltejs/svelte/pull/10736))
## 5.0.0-next.73
### Patch Changes
- fix: improve bind:this support around proxyied state ([#10732](https://github.com/sveltejs/svelte/pull/10732))
- fix: bump specificity on all members of a selector list ([#10730](https://github.com/sveltejs/svelte/pull/10730))
- breaking: preserve slots inside templates with a shadowrootmode attribute ([#10721](https://github.com/sveltejs/svelte/pull/10721))
- chore: custom elements validation ([#10720](https://github.com/sveltejs/svelte/pull/10720))
- fix: ensure performance.now() and requestAnimationFrame() are polyfilled in ssr ([#10715](https://github.com/sveltejs/svelte/pull/10715))
- fix: eagerly unsubscribe when store is changed ([#10727](https://github.com/sveltejs/svelte/pull/10727))
- fix: error when exporting reassigned state from module context ([#10728](https://github.com/sveltejs/svelte/pull/10728))
## 5.0.0-next.72
### Patch Changes
- fix: adjust keyed each block equality handling ([#10699](https://github.com/sveltejs/svelte/pull/10699))
- fix: improve indexed each equality ([#10702](https://github.com/sveltejs/svelte/pull/10702))
- fix: prevent snippet children conflict ([#10700](https://github.com/sveltejs/svelte/pull/10700))
## 5.0.0-next.71
### Patch Changes
- fix: improve namespace inference when having `{@render}` and `{@html}` tags ([#10631](https://github.com/sveltejs/svelte/pull/10631))
- fix: don't collapse whitespace within text nodes ([#10691](https://github.com/sveltejs/svelte/pull/10691))
## 5.0.0-next.70
### Patch Changes
- fix: better ownership mutation validation ([#10673](https://github.com/sveltejs/svelte/pull/10673))
- fix: handle TypeScript's optional parameter syntax in snippets ([#10671](https://github.com/sveltejs/svelte/pull/10671))
- fix: deduplicate generated props and action arg names ([#10669](https://github.com/sveltejs/svelte/pull/10669))
## 5.0.0-next.69
### Patch Changes
- perf: bail early when traversing non-state ([#10654](https://github.com/sveltejs/svelte/pull/10654))
- feat: improve ssr html mismatch validation ([#10658](https://github.com/sveltejs/svelte/pull/10658))
- fix: improve ssr output of dynamic textarea elements ([#10638](https://github.com/sveltejs/svelte/pull/10638))
- fix: improve ssr code generation for class property $derived ([#10661](https://github.com/sveltejs/svelte/pull/10661))
- fix: warn when `$props` rune not called ([#10655](https://github.com/sveltejs/svelte/pull/10655))
- fix: improve derived rune destructuring support ([#10665](https://github.com/sveltejs/svelte/pull/10665))
- feat: allow arbitrary call expressions and optional chaining for snippets ([#10656](https://github.com/sveltejs/svelte/pull/10656))
- fix: add `$set` and `$on` methods in legacy compat mode ([#10642](https://github.com/sveltejs/svelte/pull/10642))
## 5.0.0-next.68
### Patch Changes
- fix: improve deep_read performance ([#10624](https://github.com/sveltejs/svelte/pull/10624))
## 5.0.0-next.67
### Patch Changes
- fix: improve event delegation with shadowed bindings ([#10620](https://github.com/sveltejs/svelte/pull/10620))
- feat: add reactive Date object to svelte/reactivity ([#10622](https://github.com/sveltejs/svelte/pull/10622))
## 5.0.0-next.66
### Patch Changes
- fix: don't clear date input on temporarily invalid value ([#10616](https://github.com/sveltejs/svelte/pull/10616))
- fix: use safe-equals comparison for `@const` tags in legacy mode ([#10606](https://github.com/sveltejs/svelte/pull/10606))
- fix: improve proxy effect dependency tracking ([#10605](https://github.com/sveltejs/svelte/pull/10605))
- fix: prevent window listeners from triggering events twice ([#10611](https://github.com/sveltejs/svelte/pull/10611))
- feat: allow dynamic `type` attribute with `bind:value` ([#10608](https://github.com/sveltejs/svelte/pull/10608))
- fix: make `bind_this` implementation more robust ([#10598](https://github.com/sveltejs/svelte/pull/10598))
- fix: tweak initial `bind:clientWidth/clientHeight/offsetWidth/offsetHeight` update timing ([#10512](https://github.com/sveltejs/svelte/pull/10512))
- fix: correctly handle proxied signal writes before reads ([#10612](https://github.com/sveltejs/svelte/pull/10612))
## 5.0.0-next.65 ## 5.0.0-next.65
### Patch Changes ### Patch Changes
@ -642,7 +780,7 @@
- fix: allow `bind:this` with dynamic type on inputs ([#9713](https://github.com/sveltejs/svelte/pull/9713)) - fix: allow `bind:this` with dynamic type on inputs ([#9713](https://github.com/sveltejs/svelte/pull/9713))
- fix: port over props that were set prior to initialization ([#9704](https://github.com/sveltejs/svelte/pull/9704)) - fix: port over props that were set prior to initialisation ([#9704](https://github.com/sveltejs/svelte/pull/9704))
- feat: $inspect rune ([#9705](https://github.com/sveltejs/svelte/pull/9705)) - feat: $inspect rune ([#9705](https://github.com/sveltejs/svelte/pull/9705))

@ -67,7 +67,7 @@ export type ToggleEventHandler<T extends EventTarget> = EventHandler<ToggleEvent
export interface DOMAttributes<T extends EventTarget> { export interface DOMAttributes<T extends EventTarget> {
// Implicit children prop every element has // Implicit children prop every element has
// Add this here so that libraries doing `$props<HTMLButtonAttributes>()` don't need a separate interface // Add this here so that libraries doing `let { ...props }: HTMLButtonAttributes = $props()` don't need a separate interface
children?: import('svelte').Snippet; children?: import('svelte').Snippet;
// Clipboard Events // Clipboard Events

@ -2,7 +2,7 @@
"name": "svelte", "name": "svelte",
"description": "Cybernetically enhanced web apps", "description": "Cybernetically enhanced web apps",
"license": "MIT", "license": "MIT",
"version": "5.0.0-next.65", "version": "5.0.0-next.78",
"type": "module", "type": "module",
"types": "./types/index.d.ts", "types": "./types/index.d.ts",
"engines": { "engines": {
@ -62,6 +62,10 @@
"types": "./types/index.d.ts", "types": "./types/index.d.ts",
"default": "./src/motion/index.js" "default": "./src/motion/index.js"
}, },
"./reactivity": {
"types": "./types/index.d.ts",
"default": "./src/reactivity/index.js"
},
"./server": { "./server": {
"types": "./types/index.d.ts", "types": "./types/index.d.ts",
"default": "./src/server/index.js" "default": "./src/server/index.js"

@ -89,8 +89,10 @@ const parse = {
'duplicate-style-element': () => `A component can have a single top-level <style> element`, 'duplicate-style-element': () => `A component can have a single top-level <style> element`,
'duplicate-script-element': () => 'duplicate-script-element': () =>
`A component can have a single top-level <script> element and/or a single top-level <script context="module"> element`, `A component can have a single top-level <script> element and/or a single top-level <script context="module"> element`,
'invalid-render-expression': () => 'expected an identifier followed by (...)', 'invalid-render-expression': () => '{@render ...} tags can only contain call expressions',
'invalid-render-arguments': () => 'expected at most one argument', 'invalid-render-arguments': () => 'expected at most one argument',
'invalid-render-call': () =>
'Calling a snippet function using apply, bind or call is not allowed',
'invalid-render-spread-argument': () => 'cannot use spread arguments in {@render ...} tags', 'invalid-render-spread-argument': () => 'cannot use spread arguments in {@render ...} tags',
'invalid-snippet-rest-parameter': () => 'invalid-snippet-rest-parameter': () =>
'snippets do not support rest parameters; use an array instead' 'snippets do not support rest parameters; use an array instead'
@ -108,7 +110,8 @@ const css = {
`:global(...) must not contain type or universal selectors when used in a compound selector`, `:global(...) must not contain type or universal selectors when used in a compound selector`,
'invalid-css-selector': () => `Invalid selector`, 'invalid-css-selector': () => `Invalid selector`,
'invalid-css-identifier': () => 'Expected a valid CSS identifier', 'invalid-css-identifier': () => 'Expected a valid CSS identifier',
'invalid-nesting-selector': () => `Nesting selectors can only be used inside a rule` 'invalid-nesting-selector': () => `Nesting selectors can only be used inside a rule`,
'invalid-css-declaration': () => 'Declaration cannot be empty'
}; };
/** @satisfies {Errors} */ /** @satisfies {Errors} */
@ -276,7 +279,9 @@ const attributes = {
directive2 directive2
)} directive`; )} directive`;
}, },
'invalid-let-directive-placement': () => 'let directive at invalid position' 'invalid-let-directive-placement': () => 'let directive at invalid position',
'invalid-style-directive-modifier': () =>
`Invalid 'style:' modifier. Valid modifiers are: 'important'`
}; };
/** @satisfies {Errors} */ /** @satisfies {Errors} */
@ -294,7 +299,9 @@ const slots = {
/** @param {string} name @param {string} component */ /** @param {string} name @param {string} component */
'duplicate-slot-name': (name, component) => `Duplicate slot name '${name}' in <${component}>`, 'duplicate-slot-name': (name, component) => `Duplicate slot name '${name}' in <${component}>`,
'invalid-default-slot-content': () => 'invalid-default-slot-content': () =>
`Found default slot content alongside an explicit slot="default"` `Found default slot content alongside an explicit slot="default"`,
'conflicting-children-snippet': () =>
`Cannot use explicit children snippet at the same time as implicit children content. Remove either the non-whitespace content or the children snippet block`
}; };
/** @satisfies {Errors} */ /** @satisfies {Errors} */
@ -326,7 +333,11 @@ const variables = {
`${name} is an illegal variable name. To reference a global variable called ${name}, use globalThis.${name}`, `${name} is an illegal variable name. To reference a global variable called ${name}, use globalThis.${name}`,
/** @param {string} name */ /** @param {string} name */
'duplicate-declaration': (name) => `'${name}' has already been declared`, 'duplicate-declaration': (name) => `'${name}' has already been declared`,
'default-export': () => `A component cannot have a default export` 'default-export': () => `A component cannot have a default export`,
'illegal-variable-declaration': () =>
'Cannot declare same variable name which is imported inside <script context="module">',
'illegal-store-subscription': () =>
'Cannot subscribe to stores that are not declared at the top level of the component'
}; };
/** @satisfies {Errors} */ /** @satisfies {Errors} */
@ -431,11 +442,6 @@ const errors = {
// message // message
// }; // };
// }, // },
// contextual_store: {
// code: 'contextual-store',
// message:
// 'Stores must be declared at the top level of the component (this may change in a future version of Svelte)'
// },
// default_export: { // default_export: {
// code: 'default-export', // code: 'default-export',
// message: 'A component cannot have a default export' // message: 'A component cannot have a default export'
@ -444,31 +450,11 @@ const errors = {
// code: 'illegal-declaration', // code: 'illegal-declaration',
// message: 'The $ prefix is reserved, and cannot be used for variable and import names' // message: 'The $ prefix is reserved, and cannot be used for variable and import names'
// }, // },
// illegal_global: /** @param {string} name */ (name) => ({
// code: 'illegal-global',
// message: `${name} is an illegal variable name`
// }),
// illegal_variable_declaration: {
// code: 'illegal-variable-declaration',
// message: 'Cannot declare same variable name which is imported inside <script context="module">'
// },
// invalid_directive_value: { // invalid_directive_value: {
// code: 'invalid-directive-value', // code: 'invalid-directive-value',
// message: // message:
// 'Can only bind to an identifier (e.g. `foo`) or a member expression (e.g. `foo.bar` or `foo[baz]`)' // 'Can only bind to an identifier (e.g. `foo`) or a member expression (e.g. `foo.bar` or `foo[baz]`)'
// }, // },
// cyclical_const_tags: /** @param {string[]} cycle */ (cycle) => ({
// code: 'cyclical-const-tags',
// message: `Cyclical dependency detected: ${cycle.join(' → ')}`
// }),
// invalid_var_declaration: {
// code: 'invalid_var_declaration',
// message: '"var" scope should not extend outside the reactive block'
// },
// invalid_style_directive_modifier: /** @param {string} valid */ (valid) => ({
// code: 'invalid-style-directive-modifier',
// message: `Valid modifiers for style directives are: ${valid}`
// })
}; };
// interface is duplicated between here (used internally) and ./interfaces.js // interface is duplicated between here (used internally) and ./interfaces.js

@ -23,10 +23,14 @@ export function compile(source, options) {
const validated = validate_component_options(options, ''); const validated = validate_component_options(options, '');
let parsed = _parse(source); let parsed = _parse(source);
const combined_options = /** @type {import('#compiler').ValidatedCompileOptions} */ ({ const { customElement: customElementOptions, ...parsed_options } = parsed.options || {};
/** @type {import('#compiler').ValidatedCompileOptions} */
const combined_options = {
...validated, ...validated,
...parsed.options ...parsed_options,
}); customElementOptions
};
if (parsed.metadata.ts) { if (parsed.metadata.ts) {
parsed = { parsed = {

@ -13,16 +13,17 @@ import { error } from '../../../errors.js';
/** /**
* @param {import('../index.js').Parser} parser * @param {import('../index.js').Parser} parser
* @param {boolean} [optional_allowed]
* @returns {import('estree').Pattern} * @returns {import('estree').Pattern}
*/ */
export default function read_pattern(parser) { export default function read_pattern(parser, optional_allowed = false) {
const start = parser.index; const start = parser.index;
let i = parser.index; let i = parser.index;
const code = full_char_code_at(parser.template, i); const code = full_char_code_at(parser.template, i);
if (isIdentifierStart(code, true)) { if (isIdentifierStart(code, true)) {
const name = /** @type {string} */ (parser.read_identifier()); const name = /** @type {string} */ (parser.read_identifier());
const annotation = read_type_annotation(parser); const annotation = read_type_annotation(parser, optional_allowed);
return { return {
type: 'Identifier', type: 'Identifier',
@ -83,7 +84,7 @@ export default function read_pattern(parser) {
parse_expression_at(`${space_with_newline}(${pattern_string} = 1)`, parser.ts, start - 1) parse_expression_at(`${space_with_newline}(${pattern_string} = 1)`, parser.ts, start - 1)
).left; ).left;
expression.typeAnnotation = read_type_annotation(parser); expression.typeAnnotation = read_type_annotation(parser, optional_allowed);
if (expression.typeAnnotation) { if (expression.typeAnnotation) {
expression.end = expression.typeAnnotation.end; expression.end = expression.typeAnnotation.end;
} }
@ -96,12 +97,19 @@ export default function read_pattern(parser) {
/** /**
* @param {import('../index.js').Parser} parser * @param {import('../index.js').Parser} parser
* @param {boolean} [optional_allowed]
* @returns {any} * @returns {any}
*/ */
function read_type_annotation(parser) { function read_type_annotation(parser, optional_allowed = false) {
const start = parser.index; const start = parser.index;
parser.allow_whitespace(); parser.allow_whitespace();
if (optional_allowed && parser.eat('?')) {
// Acorn-TS puts the optional info as a property on the surrounding node.
// We spare the work here because it doesn't matter for us anywhere else.
parser.allow_whitespace();
}
if (!parser.eat(':')) { if (!parser.eat(':')) {
parser.index = start; parser.index = start;
return undefined; return undefined;

@ -54,6 +54,11 @@ export default function read_options(node) {
component_options.customElement = ce; component_options.customElement = ce;
break; break;
} else if (value[0].expression.type !== 'ObjectExpression') { } else if (value[0].expression.type !== 'ObjectExpression') {
// Before Svelte 4 it was necessary to explicitly set customElement to null or else you'd get a warning.
// This is no longer necessary, but for backwards compat just skip in this case now.
if (value[0].expression.type === 'Literal' && value[0].expression.value === null) {
break;
}
error(attribute, 'invalid-svelte-option-customElement'); error(attribute, 'invalid-svelte-option-customElement');
} }

@ -476,6 +476,10 @@ function read_declaration(parser) {
const value = read_value(parser); const value = read_value(parser);
if (!value && !property.startsWith('--')) {
error(parser.index, 'invalid-css-declaration');
}
const end = parser.index; const end = parser.index;
if (!parser.match('}')) { if (!parser.match('}')) {

@ -51,6 +51,23 @@ function parent_is_head(stack) {
return false; return false;
} }
/** @param {import('#compiler').TemplateNode[]} stack */
function parent_is_shadowroot_template(stack) {
// https://developer.chrome.com/docs/css-ui/declarative-shadow-dom#building_a_declarative_shadow_root
let i = stack.length;
while (i--) {
if (
stack[i].type === 'RegularElement' &&
/** @type {import('#compiler').RegularElement} */ (stack[i]).attributes.some(
(a) => a.type === 'Attribute' && a.name === 'shadowrootmode'
)
) {
return true;
}
}
return false;
}
const regex_closing_textarea_tag = /^<\/textarea(\s[^>]*)?>/i; const regex_closing_textarea_tag = /^<\/textarea(\s[^>]*)?>/i;
const regex_closing_comment = /-->/; const regex_closing_comment = /-->/;
const regex_capital_letter = /[A-Z]/; const regex_capital_letter = /[A-Z]/;
@ -112,7 +129,8 @@ export default function tag(parser) {
? 'Component' ? 'Component'
: name === 'title' && parent_is_head(parser.stack) : name === 'title' && parent_is_head(parser.stack)
? 'TitleElement' ? 'TitleElement'
: name === 'slot' : // TODO Svelte 6/7: once slots are removed in favor of snippets, always keep slot as a regular element
name === 'slot' && !parent_is_shadowroot_template(parser.stack)
? 'SlotElement' ? 'SlotElement'
: 'RegularElement'; : 'RegularElement';
@ -268,22 +286,41 @@ export default function tag(parser) {
if (name === 'script') { if (name === 'script') {
const content = read_script(parser, start, element.attributes); const content = read_script(parser, start, element.attributes);
if (content) { /** @type {import('#compiler').Comment | null} */
if (content.context === 'module') { let prev_comment = null;
if (current.module) error(start, 'duplicate-script-element'); for (let i = current.fragment.nodes.length - 1; i >= 0; i--) {
current.module = content; const node = current.fragment.nodes[i];
} else {
if (current.instance) error(start, 'duplicate-script-element'); if (i === current.fragment.nodes.length - 1 && node.end !== start) {
current.instance = content; break;
} }
if (node.type === 'Comment') {
prev_comment = node;
break;
} else if (node.type !== 'Text' || node.data.trim()) {
break;
}
}
if (prev_comment) {
// We take advantage of the fact that the root will never have leadingComments set,
// and set the previous comment to it so that the warning mechanism can later
// inspect the root and see if there was a html comment before it silencing specific warnings.
content.content.leadingComments = [{ type: 'Line', value: prev_comment.data }];
}
if (content.context === 'module') {
if (current.module) error(start, 'duplicate-script-element');
current.module = content;
} else {
if (current.instance) error(start, 'duplicate-script-element');
current.instance = content;
} }
} else { } else {
const content = read_style(parser, start, element.attributes); const content = read_style(parser, start, element.attributes);
if (content) { if (current.css) error(start, 'duplicate-style-element');
if (current.css) error(start, 'duplicate-style-element'); current.css = content;
current.css = content;
}
} }
return; return;
} }

@ -276,7 +276,7 @@ function open(parser) {
const parameters = []; const parameters = [];
while (!parser.match(')')) { while (!parser.match(')')) {
let pattern = read_pattern(parser); let pattern = read_pattern(parser, true);
parser.allow_whitespace(); parser.allow_whitespace();
if (parser.eat('=')) { if (parser.eat('=')) {
@ -577,7 +577,12 @@ function special(parser) {
const expression = read_expression(parser); const expression = read_expression(parser);
if (expression.type !== 'CallExpression' || expression.callee.type !== 'Identifier') { if (
expression.type !== 'CallExpression' &&
(expression.type !== 'ChainExpression' ||
expression.expression.type !== 'CallExpression' ||
!expression.expression.optional)
) {
error(expression, 'invalid-render-expression'); error(expression, 'invalid-render-expression');
} }
@ -589,8 +594,7 @@ function special(parser) {
type: 'RenderTag', type: 'RenderTag',
start, start,
end: parser.index, end: parser.index,
expression: expression.callee, expression: expression
arguments: expression.arguments
}); });
} }
} }

@ -132,7 +132,7 @@ class FuzzySet {
/** @param {string[]} arr */ /** @param {string[]} arr */
constructor(arr) { constructor(arr) {
// initialization // initialisation
for (let i = GRAM_SIZE_LOWER; i < GRAM_SIZE_UPPER + 1; ++i) { for (let i = GRAM_SIZE_LOWER; i < GRAM_SIZE_UPPER + 1; ++i) {
this.items[i] = []; this.items[i] = [];
} }

@ -1,3 +1,4 @@
import { interactive_elements } from '../../../../constants.js';
import entities from './entities.js'; import entities from './entities.js';
const windows_1252 = [ const windows_1252 = [
@ -121,16 +122,6 @@ function validate_code(code) {
// based on http://developers.whatwg.org/syntax.html#syntax-tag-omission // based on http://developers.whatwg.org/syntax.html#syntax-tag-omission
// while `input` is also an interactive element, it is never moved by the browser, so we don't need to check for it
export const interactive_elements = new Set([
'a',
'button',
'iframe',
'embed',
'select',
'textarea'
]);
/** @type {Record<string, Set<string>>} */ /** @type {Record<string, Set<string>>} */
const disallowed_contents = { const disallowed_contents = {
li: new Set(['li']), li: new Set(['li']),
@ -153,36 +144,6 @@ const disallowed_contents = {
th: new Set(['td', 'th', 'tr']) th: new Set(['td', 'th', 'tr'])
}; };
export const disallowed_parapgraph_contents = [
'address',
'article',
'aside',
'blockquote',
'details',
'div',
'dl',
'fieldset',
'figcapture',
'figure',
'footer',
'form',
'h1',
'h2',
'h3',
'h4',
'h5',
'h6',
'header',
'hr',
'menu',
'nav',
'ol',
'pre',
'section',
'table',
'ul'
];
for (const interactive_element of interactive_elements) { for (const interactive_element of interactive_elements) {
disallowed_contents[interactive_element] = interactive_elements; disallowed_contents[interactive_element] = interactive_elements;
} }

@ -176,6 +176,12 @@ function get_delegated_event(event_name, handler, context) {
return non_hoistable; return non_hoistable;
} }
const binding = scope.get(reference); const binding = scope.get(reference);
const local_binding = context.state.scope.get(reference);
// 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 non_hoistable;
}
// If we have multiple references to the same store using $ prefix, bail out. // If we have multiple references to the same store using $ prefix, bail out.
if ( if (
@ -294,6 +300,25 @@ export function analyze_component(root, options) {
declaration.initial.source.value === 'svelte/store' declaration.initial.source.value === 'svelte/store'
)) ))
) { ) {
let is_nested_store_subscription = false;
for (const reference of references) {
for (let i = reference.path.length - 1; i >= 0; i--) {
const scope =
scopes.get(reference.path[i]) ||
module.scopes.get(reference.path[i]) ||
instance.scopes.get(reference.path[i]);
if (scope) {
const owner = scope?.owner(store_name);
is_nested_store_subscription =
!!owner && owner !== module.scope && owner !== instance.scope;
break;
}
}
}
if (is_nested_store_subscription) {
error(references[0].node, 'illegal-store-subscription');
}
if (options.runes !== false) { if (options.runes !== false) {
if (declaration === null && /[a-z]/.test(store_name[0])) { if (declaration === null && /[a-z]/.test(store_name[0])) {
error(references[0].node, 'illegal-global', name); error(references[0].node, 'illegal-global', name);
@ -349,9 +374,13 @@ export function analyze_component(root, options) {
uses_rest_props: false, uses_rest_props: false,
uses_slots: false, uses_slots: false,
uses_component_bindings: false, uses_component_bindings: false,
custom_element: options.customElement, custom_element: options.customElementOptions ?? options.customElement,
inject_styles: options.css === 'injected' || !!options.customElement, inject_styles: options.css === 'injected' || options.customElement,
accessors: options.customElement ? true : !!options.accessors, accessors: options.customElement
? true
: !!options.accessors ||
// because $set method needs accessors
!!options.legacy?.componentApi,
reactive_statements: new Map(), reactive_statements: new Map(),
binding_groups: new Map(), binding_groups: new Map(),
slot_names: new Set(), slot_names: new Set(),
@ -370,6 +399,10 @@ export function analyze_component(root, options) {
} }
}; };
if (!options.customElement && root.options?.customElement) {
warn(analysis.warnings, root.options, [], 'missing-custom-element-compile-option');
}
if (analysis.runes) { if (analysis.runes) {
const props_refs = module.scope.references.get('$$props'); const props_refs = module.scope.references.get('$$props');
if (props_refs) { if (props_refs) {
@ -432,6 +465,17 @@ export function analyze_component(root, options) {
); );
} }
for (const [name, binding] of instance.scope.declarations) {
if (binding.kind === 'prop' && binding.node.name !== '$$props') {
const references = binding.references.filter(
(r) => r.node !== binding.node && r.path.at(-1)?.type !== 'ExportSpecifier'
);
if (!references.length && !instance.scope.declarations.has(`$${name}`)) {
warn(warnings, binding.node, [], 'unused-export-let', name);
}
}
}
analysis.reactive_statements = order_reactive_statements(analysis.reactive_statements); analysis.reactive_statements = order_reactive_statements(analysis.reactive_statements);
} }
@ -450,6 +494,25 @@ export function analyze_component(root, options) {
) { ) {
continue inner; continue inner;
} }
// bind:this doesn't need to be a state reference if it will never change
if (
type === 'BindDirective' &&
/** @type {import('#compiler').BindDirective} */ (path[i]).name === 'this'
) {
for (let j = i - 1; j >= 0; j -= 1) {
const type = path[j].type;
if (
type === 'IfBlock' ||
type === 'EachBlock' ||
type === 'AwaitBlock' ||
type === 'KeyBlock'
) {
warn(warnings, binding.node, [], 'non-state-reference', name);
continue outer;
}
}
continue inner;
}
} }
warn(warnings, binding.node, [], 'non-state-reference', name); warn(warnings, binding.node, [], 'non-state-reference', name);
@ -561,6 +624,17 @@ const legacy_scope_tweaker = {
state.reactive_statements.set(node, reactive_statement); state.reactive_statements.set(node, reactive_statement);
// Ideally this would be in the validation file, but that isn't possible because this visitor
// calls "next" before setting the reactive statements.
if (
reactive_statement.dependencies.size &&
[...reactive_statement.dependencies].every(
(d) => d.scope === state.analysis.module.scope && d.declaration_kind !== 'const'
)
) {
warn(state.analysis.warnings, node, path, 'module-script-reactive-declaration');
}
if ( if (
node.body.type === 'ExpressionStatement' && node.body.type === 'ExpressionStatement' &&
node.body.expression.type === 'AssignmentExpression' node.body.expression.type === 'AssignmentExpression'
@ -681,7 +755,8 @@ const legacy_scope_tweaker = {
if ( if (
binding.kind === 'state' || binding.kind === 'state' ||
binding.kind === 'frozen_state' || binding.kind === 'frozen_state' ||
(binding.kind === 'normal' && binding.declaration_kind === 'let') (binding.kind === 'normal' &&
(binding.declaration_kind === 'let' || binding.declaration_kind === 'var'))
) { ) {
binding.kind = 'prop'; binding.kind = 'prop';
if (specifier.exported.name !== specifier.local.name) { if (specifier.exported.name !== specifier.local.name) {

@ -1,14 +1,19 @@
import {
disallowed_paragraph_contents,
interactive_elements,
is_tag_valid_with_parent
} from '../../../constants.js';
import { error } from '../../errors.js'; import { error } from '../../errors.js';
import { import {
extract_identifiers, extract_identifiers,
get_parent, get_parent,
is_expression_attribute, is_expression_attribute,
is_text_attribute, is_text_attribute,
object object,
unwrap_optional
} from '../../utils/ast.js'; } from '../../utils/ast.js';
import { warn } from '../../warnings.js'; import { warn } from '../../warnings.js';
import fuzzymatch from '../1-parse/utils/fuzzymatch.js'; import fuzzymatch from '../1-parse/utils/fuzzymatch.js';
import { disallowed_parapgraph_contents, interactive_elements } from '../1-parse/utils/html.js';
import { binding_properties } from '../bindings.js'; import { binding_properties } from '../bindings.js';
import { ContentEditableBindings, EventModifiers, SVGElements } from '../constants.js'; import { ContentEditableBindings, EventModifiers, SVGElements } from '../constants.js';
import { is_custom_element_node } from '../nodes.js'; import { is_custom_element_node } from '../nodes.js';
@ -44,8 +49,12 @@ function validate_component(node, context) {
error(attribute, 'invalid-event-modifier'); error(attribute, 'invalid-event-modifier');
} }
if (attribute.type === 'Attribute' && attribute.name === 'slot') { if (attribute.type === 'Attribute') {
validate_slot_attribute(context, attribute); validate_attribute_name(attribute, context);
if (attribute.name === 'slot') {
validate_slot_attribute(context, attribute);
}
} }
} }
@ -56,6 +65,11 @@ function validate_component(node, context) {
}); });
} }
const react_attributes = new Map([
['className', 'class'],
['htmlFor', 'for']
]);
/** /**
* @param {import('#compiler').RegularElement | import('#compiler').SvelteElement} node * @param {import('#compiler').RegularElement | import('#compiler').SvelteElement} node
* @param {import('zimmerframe').Context<import('#compiler').SvelteNode, import('./types.js').AnalysisState>} context * @param {import('zimmerframe').Context<import('#compiler').SvelteNode, import('./types.js').AnalysisState>} context
@ -100,6 +114,20 @@ function validate_element(node, context) {
if (attribute.name === 'is' && context.state.options.namespace !== 'foreign') { if (attribute.name === 'is' && context.state.options.namespace !== 'foreign') {
warn(context.state.analysis.warnings, attribute, context.path, 'avoid-is'); warn(context.state.analysis.warnings, attribute, context.path, 'avoid-is');
} }
const correct_name = react_attributes.get(attribute.name);
if (correct_name) {
warn(
context.state.analysis.warnings,
attribute,
context.path,
'invalid-html-attribute',
attribute.name,
correct_name
);
}
validate_attribute_name(attribute, context);
} else if (attribute.type === 'AnimateDirective') { } else if (attribute.type === 'AnimateDirective') {
const parent = context.path.at(-2); const parent = context.path.at(-2);
if (parent?.type !== 'EachBlock') { if (parent?.type !== 'EachBlock') {
@ -161,6 +189,21 @@ function validate_element(node, context) {
} }
} }
/**
* @param {import('#compiler').Attribute} attribute
* @param {import('zimmerframe').Context<import('#compiler').SvelteNode, import('./types.js').AnalysisState>} context
*/
function validate_attribute_name(attribute, context) {
if (
attribute.name.includes(':') &&
!attribute.name.startsWith('xmlns:') &&
!attribute.name.startsWith('xlink:') &&
!attribute.name.startsWith('xml:')
) {
warn(context.state.analysis.warnings, attribute, context.path, 'illegal-attribute-character');
}
}
/** /**
* @param {import('zimmerframe').Context<import('#compiler').SvelteNode, import('./types.js').AnalysisState>} context * @param {import('zimmerframe').Context<import('#compiler').SvelteNode, import('./types.js').AnalysisState>} context
* @param {import('#compiler').Attribute} attribute * @param {import('#compiler').Attribute} attribute
@ -226,125 +269,17 @@ function validate_slot_attribute(context, attribute) {
} }
} }
// https://html.spec.whatwg.org/multipage/syntax.html#generate-implied-end-tags
const implied_end_tags = ['dd', 'dt', 'li', 'option', 'optgroup', 'p', 'rp', 'rt'];
/** /**
* @param {string} tag * @param {import('#compiler').Fragment | null | undefined} node
* @param {string} parent_tag * @param {import('zimmerframe').Context<import('#compiler').SvelteNode, import('./types.js').AnalysisState>} context
* @returns {boolean}
*/ */
function is_tag_valid_with_parent(tag, parent_tag) { function validate_block_not_empty(node, context) {
// First, let's check if we're in an unusual parsing mode... if (!node) return;
switch (parent_tag) { // Assumption: If the block has zero elements, someone's in the middle of typing it out,
// https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-inselect // so don't warn in that case because it would be distracting.
case 'select': if (node.nodes.length === 1 && node.nodes[0].type === 'Text' && !node.nodes[0].raw.trim()) {
return tag === 'option' || tag === 'optgroup' || tag === '#text'; warn(context.state.analysis.warnings, node.nodes[0], context.path, 'empty-block');
case 'optgroup':
return tag === 'option' || tag === '#text';
// Strictly speaking, seeing an <option> doesn't mean we're in a <select>
// but
case 'option':
return tag === '#text';
// https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-intd
// https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-incaption
// No special behavior since these rules fall back to "in body" mode for
// all except special table nodes which cause bad parsing behavior anyway.
// https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-intr
case 'tr':
return (
tag === 'th' || tag === 'td' || tag === 'style' || tag === 'script' || tag === 'template'
);
// https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-intbody
case 'tbody':
case 'thead':
case 'tfoot':
return tag === 'tr' || tag === 'style' || tag === 'script' || tag === 'template';
// https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-incolgroup
case 'colgroup':
return tag === 'col' || tag === 'template';
// https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-intable
case 'table':
return (
tag === 'caption' ||
tag === 'colgroup' ||
tag === 'tbody' ||
tag === 'tfoot' ||
tag === 'thead' ||
tag === 'style' ||
tag === 'script' ||
tag === 'template'
);
// https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-inhead
case 'head':
return (
tag === 'base' ||
tag === 'basefont' ||
tag === 'bgsound' ||
tag === 'link' ||
tag === 'meta' ||
tag === 'title' ||
tag === 'noscript' ||
tag === 'noframes' ||
tag === 'style' ||
tag === 'script' ||
tag === 'template'
);
// https://html.spec.whatwg.org/multipage/semantics.html#the-html-element
case 'html':
return tag === 'head' || tag === 'body' || tag === 'frameset';
case 'frameset':
return tag === 'frame';
case '#document':
return tag === 'html';
}
// Probably in the "in body" parsing mode, so we outlaw only tag combos
// where the parsing rules cause implicit opens or closes to be added.
// https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-inbody
switch (tag) {
case 'h1':
case 'h2':
case 'h3':
case 'h4':
case 'h5':
case 'h6':
return (
parent_tag !== 'h1' &&
parent_tag !== 'h2' &&
parent_tag !== 'h3' &&
parent_tag !== 'h4' &&
parent_tag !== 'h5' &&
parent_tag !== 'h6'
);
case 'rp':
case 'rt':
return implied_end_tags.indexOf(parent_tag) === -1;
case 'body':
case 'caption':
case 'col':
case 'colgroup':
case 'frameset':
case 'frame':
case 'head':
case 'html':
case 'tbody':
case 'td':
case 'tfoot':
case 'th':
case 'thead':
case 'tr':
// These tags are only valid with a few parents that have special child
// parsing rules -- if we're down here, then none of those matched and
// so we allow it only if we don't know what the parent is, as all other
// cases are invalid.
return parent_tag == null;
} }
return true;
} }
/** /**
@ -393,13 +328,24 @@ const validation = {
// TODO handle mutations of non-state/props in runes mode // TODO handle mutations of non-state/props in runes mode
} }
const binding = context.state.scope.get(left.name);
if (node.name === 'group') { if (node.name === 'group') {
const binding = context.state.scope.get(left.name);
if (!binding) { if (!binding) {
error(node, 'INTERNAL', 'Cannot find declaration for bind:group'); error(node, 'INTERNAL', 'Cannot find declaration for bind:group');
} }
} }
if (binding?.kind === 'each' && binding.metadata?.inside_rest) {
warn(
context.state.analysis.warnings,
binding.node,
context.path,
'invalid-rest-eachblock-binding',
binding.node.name
);
}
const parent = context.path.at(-1); const parent = context.path.at(-1);
if ( if (
@ -589,7 +535,7 @@ const validation = {
} }
} }
if (disallowed_parapgraph_contents.includes(node.name)) { if (disallowed_paragraph_contents.includes(node.name)) {
const path = context.path; const path = context.path;
for (let parent of path) { for (let parent of path) {
if (parent.type === 'RegularElement' && parent.name === 'p') { if (parent.type === 'RegularElement' && parent.name === 'p') {
@ -604,11 +550,81 @@ const validation = {
}); });
}, },
RenderTag(node, context) { RenderTag(node, context) {
for (const arg of node.arguments) { const raw_args = unwrap_optional(node.expression).arguments;
for (const arg of raw_args) {
if (arg.type === 'SpreadElement') { if (arg.type === 'SpreadElement') {
error(arg, 'invalid-render-spread-argument'); error(arg, 'invalid-render-spread-argument');
} }
} }
const callee = unwrap_optional(node.expression).callee;
if (
callee.type === 'MemberExpression' &&
callee.property.type === 'Identifier' &&
['bind', 'apply', 'call'].includes(callee.property.name)
) {
error(node, 'invalid-render-call');
}
const is_inside_textarea = context.path.find((n) => {
return (
n.type === 'SvelteElement' &&
n.name === 'svelte:element' &&
n.tag.type === 'Literal' &&
n.tag.value === 'textarea'
);
});
if (is_inside_textarea) {
error(
node,
'invalid-tag-placement',
'inside <textarea> or <svelte:element this="textarea">',
'render'
);
}
},
IfBlock(node, context) {
validate_block_not_empty(node.consequent, context);
validate_block_not_empty(node.alternate, context);
},
EachBlock(node, context) {
validate_block_not_empty(node.body, context);
validate_block_not_empty(node.fallback, context);
},
AwaitBlock(node, context) {
validate_block_not_empty(node.pending, context);
validate_block_not_empty(node.then, context);
validate_block_not_empty(node.catch, context);
},
KeyBlock(node, context) {
validate_block_not_empty(node.fragment, context);
},
SnippetBlock(node, context) {
validate_block_not_empty(node.body, context);
if (node.expression.name !== 'children') return;
const { path } = context;
const parent = path.at(-2);
if (!parent) return;
if (
parent.type === 'Component' ||
parent.type === 'SvelteComponent' ||
parent.type === 'SvelteSelf'
) {
if (
parent.fragment.nodes.some(
(node) => node.type !== 'SnippetBlock' && (node.type !== 'Text' || node.data.trim())
)
) {
error(node, 'conflicting-children-snippet');
}
}
},
StyleDirective(node) {
if (node.modifiers.length > 1 || (node.modifiers.length && node.modifiers[0] !== 'important')) {
error(node, 'invalid-style-directive-modifier');
}
}, },
SvelteHead(node) { SvelteHead(node) {
const attribute = node.attributes[0]; const attribute = node.attributes[0];
@ -693,6 +709,8 @@ const validation = {
export const validation_legacy = merge(validation, a11y_validators, { export const validation_legacy = merge(validation, a11y_validators, {
VariableDeclarator(node, { state }) { VariableDeclarator(node, { state }) {
ensure_no_module_import_conflict(node, state);
if (node.init?.type !== 'CallExpression') return; if (node.init?.type !== 'CallExpression') return;
const callee = node.init.callee; const callee = node.init.callee;
@ -803,6 +821,22 @@ function validate_call_expression(node, scope, path) {
} }
} }
/**
* @param {import('estree').VariableDeclarator} node
* @param {import('./types.js').AnalysisState} state
*/
function ensure_no_module_import_conflict(node, state) {
const ids = extract_identifiers(node.id);
for (const id of ids) {
if (
state.scope === state.analysis.instance.scope &&
state.analysis.module.scope.get(id.name)?.declaration_kind === 'import'
) {
error(node.id, 'illegal-variable-declaration');
}
}
}
/** /**
* @type {import('zimmerframe').Visitors<import('#compiler').SvelteNode, import('./types.js').AnalysisState>} * @type {import('zimmerframe').Visitors<import('#compiler').SvelteNode, import('./types.js').AnalysisState>}
*/ */
@ -963,11 +997,29 @@ export const validation_runes = merge(validation, a11y_validators, {
if (node.label.name !== '$' || path.at(-1)?.type !== 'Program') return; if (node.label.name !== '$' || path.at(-1)?.type !== 'Program') return;
error(node, 'invalid-legacy-reactive-statement'); error(node, 'invalid-legacy-reactive-statement');
}, },
ExportNamedDeclaration(node, { state }) { ExportNamedDeclaration(node, { state, next }) {
if (node.declaration?.type !== 'VariableDeclaration') return; if (state.ast_type === 'module') {
if (node.declaration.kind !== 'let') return; if (node.declaration?.type !== 'VariableDeclaration') return;
if (state.analysis.instance.scope !== state.scope) return;
error(node, 'invalid-legacy-export'); // visit children, so bindings are correctly initialised
next();
for (const declarator of node.declaration.declarations) {
for (const id of extract_identifiers(declarator.id)) {
validate_export(node, state.scope, id.name);
}
}
} else {
if (node.declaration?.type !== 'VariableDeclaration') return;
if (node.declaration.kind !== 'let') return;
if (state.analysis.instance.scope !== state.scope) return;
error(node, 'invalid-legacy-export');
}
},
ExportSpecifier(node, { state }) {
if (state.ast_type === 'module') {
validate_export(node, state.scope, node.local.name);
}
}, },
CallExpression(node, { state, path }) { CallExpression(node, { state, path }) {
validate_call_expression(node, state.scope, path); validate_call_expression(node, state.scope, path);
@ -983,10 +1035,17 @@ export const validation_runes = merge(validation, a11y_validators, {
next({ ...state }); next({ ...state });
}, },
VariableDeclarator(node, { state, path }) { VariableDeclarator(node, { state, path }) {
ensure_no_module_import_conflict(node, state);
const init = node.init; const init = node.init;
const rune = get_rune(init, state.scope); const rune = get_rune(init, state.scope);
if (rune === null) return; if (rune === null) {
if (init?.type === 'Identifier' && init.name === '$props' && !state.scope.get('props')) {
warn(state.analysis.warnings, node, path, 'invalid-props-declaration');
}
return;
}
const args = /** @type {import('estree').CallExpression} */ (init).arguments; const args = /** @type {import('estree').CallExpression} */ (init).arguments;

@ -214,6 +214,10 @@ export function client_component(source, analysis, options) {
instance.body.push(statement[1]); instance.body.push(statement[1]);
} }
if (analysis.reactive_statements.size > 0) {
instance.body.push(b.stmt(b.call('$.legacy_pre_effect_reset')));
}
/** /**
* Used to store the group nodes * Used to store the group nodes
* @type {import('estree').VariableDeclaration[]} * @type {import('estree').VariableDeclaration[]}
@ -258,6 +262,60 @@ export function client_component(source, analysis, options) {
} }
} }
if (options.legacy.componentApi) {
properties.push(
b.init('$set', b.id('$.update_legacy_props')),
b.init(
'$on',
b.arrow(
[b.id('$$event_name'), b.id('$$event_cb')],
b.call(
'$.add_legacy_event_listener',
b.id('$$props'),
b.id('$$event_name'),
b.id('$$event_cb')
)
)
)
);
} else if (options.dev) {
properties.push(
b.init(
'$set',
b.thunk(
b.block([
b.throw_error(
`The component shape you get when doing bind:this changed. Updating its properties via $set is no longer valid in Svelte 5. ` +
'See https://svelte-5-preview.vercel.app/docs/breaking-changes#components-are-no-longer-classes for more information'
)
])
)
),
b.init(
'$on',
b.thunk(
b.block([
b.throw_error(
`The component shape you get when doing bind:this changed. Listening to events via $on is no longer valid in Svelte 5. ` +
'See https://svelte-5-preview.vercel.app/docs/breaking-changes#components-are-no-longer-classes for more information'
)
])
)
),
b.init(
'$destroy',
b.thunk(
b.block([
b.throw_error(
`The component shape you get when doing bind:this changed. Destroying such a component via $destroy is no longer valid in Svelte 5. ` +
'See https://svelte-5-preview.vercel.app/docs/breaking-changes#components-are-no-longer-classes for more information'
)
])
)
)
);
}
const push_args = [b.id('$$props'), b.literal(analysis.runes)]; const push_args = [b.id('$$props'), b.literal(analysis.runes)];
if (options.dev) push_args.push(b.id(analysis.name)); if (options.dev) push_args.push(b.id(analysis.name));

@ -393,28 +393,37 @@ export function serialize_set_binding(node, context, fallback, options) {
return b.call(left, value); return b.call(left, value);
} else if (is_store) { } else if (is_store) {
return b.call('$.store_set', serialize_get_binding(b.id(left_name), state), value); return b.call('$.store_set', serialize_get_binding(b.id(left_name), state), value);
} else if (binding.kind === 'state') {
return b.call(
'$.set',
b.id(left_name),
context.state.analysis.runes &&
!options?.skip_proxy_and_freeze &&
should_proxy_or_freeze(value, context.state.scope)
? b.call('$.proxy', value)
: value
);
} else if (binding.kind === 'frozen_state') {
return b.call(
'$.set',
b.id(left_name),
context.state.analysis.runes &&
!options?.skip_proxy_and_freeze &&
should_proxy_or_freeze(value, context.state.scope)
? b.call('$.freeze', value)
: value
);
} else { } else {
return b.call('$.set', b.id(left_name), value); let call;
if (binding.kind === 'state') {
call = b.call(
'$.set',
b.id(left_name),
context.state.analysis.runes &&
!options?.skip_proxy_and_freeze &&
should_proxy_or_freeze(value, context.state.scope)
? b.call('$.proxy', value)
: value
);
} else if (binding.kind === 'frozen_state') {
call = b.call(
'$.set',
b.id(left_name),
context.state.analysis.runes &&
!options?.skip_proxy_and_freeze &&
should_proxy_or_freeze(value, context.state.scope)
? b.call('$.freeze', value)
: value
);
} else {
call = b.call('$.set', b.id(left_name), value);
}
if (state.scope.get(`$${left.name}`)?.kind === 'store_sub') {
return b.call('$.store_unsub', call, b.literal(`$${left.name}`), b.id('$$subscriptions'));
} else {
return call;
}
} }
} else { } else {
if (is_store) { if (is_store) {
@ -461,12 +470,14 @@ export function serialize_set_binding(node, context, fallback, options) {
if (binding.kind === 'prop') { if (binding.kind === 'prop') {
return b.call( return b.call(
left, left,
b.assignment( b.sequence([
node.operator, b.assignment(
/** @type {import('estree').Pattern} */ (visit(node.left)), node.operator,
value /** @type {import('estree').Pattern} */ (visit(node.left)),
), value
b.literal(true) ),
b.call(left)
])
); );
} else { } else {
return b.call( return b.call(

@ -17,7 +17,6 @@ export const global_visitors = {
// rewrite `this.#foo` as `this.#foo.v` inside a constructor // rewrite `this.#foo` as `this.#foo.v` inside a constructor
if (node.property.type === 'PrivateIdentifier') { if (node.property.type === 'PrivateIdentifier') {
const field = state.private_state.get(node.property.name); const field = state.private_state.get(node.property.name);
if (field) { if (field) {
return state.in_constructor ? b.member(node, b.id('v')) : b.call('$.get', node); return state.in_constructor ? b.member(node, b.id('v')) : b.call('$.get', node);
} }

@ -157,7 +157,6 @@ export const javascript_visitors_legacy = {
} }
const body = serialized_body.body; const body = serialized_body.body;
const new_body = [];
/** @type {import('estree').Expression[]} */ /** @type {import('estree').Expression[]} */
const sequence = []; const sequence = [];
@ -170,24 +169,22 @@ export const javascript_visitors_legacy = {
// If the binding is a prop, we need to deep read it because it could be fine-grained $state // If the binding is a prop, we need to deep read it because it could be fine-grained $state
// from a runes-component, where mutations don't trigger an update on the prop as a whole. // from a runes-component, where mutations don't trigger an update on the prop as a whole.
if (name === '$$props' || name === '$$restProps' || binding.kind === 'prop') { if (name === '$$props' || name === '$$restProps' || binding.kind === 'prop') {
serialized = b.call('$.deep_read', serialized); serialized = b.call('$.deep_read_state', serialized);
} }
sequence.push(serialized); sequence.push(serialized);
} }
if (sequence.length > 0) {
new_body.push(b.stmt(b.sequence(sequence)));
}
new_body.push(b.stmt(b.call('$.untrack', b.thunk(b.block(body)))));
serialized_body.body = new_body;
// these statements will be topologically ordered later // these statements will be topologically ordered later
state.legacy_reactive_statements.set( state.legacy_reactive_statements.set(
node, node,
b.stmt(b.call('$.pre_effect', b.thunk(serialized_body))) b.stmt(
b.call(
'$.legacy_pre_effect',
sequence.length > 0 ? b.thunk(b.sequence(sequence)) : b.thunk(b.block([])),
b.thunk(b.block(body))
)
)
); );
return b.empty; return b.empty;

@ -299,15 +299,22 @@ export const javascript_visitors_runes = {
); );
} else { } else {
const bindings = state.scope.get_bindings(declarator); const bindings = state.scope.get_bindings(declarator);
const id = state.scope.generate('derived_value'); const object_id = state.scope.generate('derived_object');
const values_id = state.scope.generate('derived_values');
declarations.push( declarations.push(
b.declarator( b.declarator(
b.id(id), b.id(object_id),
b.call('$.derived', b.thunk(rune === '$derived.by' ? b.call(value) : value))
)
);
declarations.push(
b.declarator(
b.id(values_id),
b.call( b.call(
'$.derived', '$.derived',
b.thunk( b.thunk(
b.block([ b.block([
b.let(declarator.id, rune === '$derived.by' ? b.call(value) : value), b.let(declarator.id, b.call('$.get', b.id(object_id))),
b.return(b.array(bindings.map((binding) => binding.node))) b.return(b.array(bindings.map((binding) => binding.node)))
]) ])
) )
@ -321,7 +328,7 @@ export const javascript_visitors_runes = {
binding.node, binding.node,
b.call( b.call(
'$.derived', '$.derived',
b.thunk(b.member(b.call('$.get', b.id(id)), b.literal(i), true)) b.thunk(b.member(b.call('$.get', b.id(values_id)), b.literal(i), true))
) )
) )
); );

@ -3,7 +3,8 @@ import {
extract_paths, extract_paths,
is_event_attribute, is_event_attribute,
is_text_attribute, is_text_attribute,
object object,
unwrap_optional
} from '../../../../utils/ast.js'; } from '../../../../utils/ast.js';
import { binding_properties } from '../../../bindings.js'; import { binding_properties } from '../../../bindings.js';
import { import {
@ -492,7 +493,7 @@ function serialize_element_attribute_update_assignment(element, node_id, attribu
let grouped_value = value; let grouped_value = value;
if (name === 'autofocus') { if (name === 'autofocus') {
state.init.push(b.stmt(b.call('$.auto_focus', node_id, value))); state.init.push(b.stmt(b.call('$.autofocus', node_id, value)));
return false; return false;
} }
@ -991,7 +992,7 @@ function serialize_inline_component(node, component_name, context) {
const prev = fn; const prev = fn;
fn = (node_id) => fn = (node_id) =>
b.call( b.call(
'$.cssProps', '$.css_props',
node_id, node_id,
// TODO would be great to do this at runtime instead. Svelte 4 also can't handle cases today // TODO would be great to do this at runtime instead. Svelte 4 also can't handle cases today
// where it's not statically determinable whether the component is used in a svg or html context // where it's not statically determinable whether the component is used in a svg or html context
@ -1799,6 +1800,12 @@ export const template_visitors = {
) )
) )
); );
// we need to eagerly evaluate the expression in order to hit any
// 'Cannot access x before initialization' errors
if (state.options.dev) {
state.init.push(b.stmt(b.call('$.get', declaration.id)));
}
} else { } else {
const identifiers = extract_identifiers(declaration.id); const identifiers = extract_identifiers(declaration.id);
const tmp = b.id(state.scope.generate('computed_const')); const tmp = b.id(state.scope.generate('computed_const'));
@ -1828,6 +1835,12 @@ export const template_visitors = {
b.const(tmp, b.call(state.options.runes ? '$.derived' : '$.derived_safe_equal', fn)) b.const(tmp, b.call(state.options.runes ? '$.derived' : '$.derived_safe_equal', fn))
); );
// we need to eagerly evaluate the expression in order to hit any
// 'Cannot access x before initialization' errors
if (state.options.dev) {
state.init.push(b.stmt(b.call('$.get', tmp)));
}
for (const node of identifiers) { for (const node of identifiers) {
const binding = /** @type {import('#compiler').Binding} */ (state.scope.get(node.name)); const binding = /** @type {import('#compiler').Binding} */ (state.scope.get(node.name));
binding.expression = b.member(b.call('$.get', tmp), node); binding.expression = b.member(b.call('$.get', tmp), node);
@ -1864,28 +1877,35 @@ export const template_visitors = {
}, },
RenderTag(node, context) { RenderTag(node, context) {
context.state.template.push('<!>'); context.state.template.push('<!>');
const binding = context.state.scope.get(node.expression.name); const callee = unwrap_optional(node.expression).callee;
const is_reactive = binding?.kind !== 'normal' || node.expression.type !== 'Identifier'; const raw_args = unwrap_optional(node.expression).arguments;
const is_reactive =
callee.type !== 'Identifier' || context.state.scope.get(callee.name)?.kind !== 'normal';
/** @type {import('estree').Expression[]} */ /** @type {import('estree').Expression[]} */
const args = [context.state.node]; const args = [context.state.node];
for (const arg of node.arguments) { for (const arg of raw_args) {
args.push(b.thunk(/** @type {import('estree').Expression} */ (context.visit(arg)))); args.push(b.thunk(/** @type {import('estree').Expression} */ (context.visit(arg))));
} }
let snippet_function = /** @type {import('estree').Expression} */ ( let snippet_function = /** @type {import('estree').Expression} */ (context.visit(callee));
context.visit(node.expression)
);
if (context.state.options.dev) { if (context.state.options.dev) {
snippet_function = b.call('$.validate_snippet', snippet_function); snippet_function = b.call('$.validate_snippet', snippet_function);
} }
if (is_reactive) { if (is_reactive) {
context.state.after_update.push( context.state.after_update.push(
b.stmt(b.call('$.snippet_effect', b.thunk(snippet_function), ...args)) b.stmt(b.call('$.snippet', b.thunk(snippet_function), ...args))
); );
} else { } else {
context.state.after_update.push(b.stmt(b.call(snippet_function, ...args))); context.state.after_update.push(
b.stmt(
(node.expression.type === 'CallExpression' ? b.call : b.maybe_call)(
snippet_function,
...args
)
)
);
} }
}, },
AnimateDirective(node, { state, visit }) { AnimateDirective(node, { state, visit }) {
@ -2324,8 +2344,20 @@ export const template_visitors = {
each_type |= EACH_IS_STRICT_EQUALS; each_type |= EACH_IS_STRICT_EQUALS;
} }
// Find the parent each blocks which contain the arrays to invalidate // If the array is a store expression, we need to invalidate it when the array is changed.
// TODO decide how much of this we want to keep for runes mode. For now we're bailing out below // This doesn't catch all cases, but all the ones that Svelte 4 catches, too.
let store_to_invalidate = '';
if (node.expression.type === 'Identifier' || node.expression.type === 'MemberExpression') {
const id = object(node.expression);
if (id) {
const binding = context.state.scope.get(id.name);
if (binding?.kind === 'store_sub') {
store_to_invalidate = id.name;
}
}
}
// Legacy mode: find the parent each blocks which contain the arrays to invalidate
const indirect_dependencies = collect_parent_each_blocks(context).flatMap((block) => { const indirect_dependencies = collect_parent_each_blocks(context).flatMap((block) => {
const array = /** @type {import('estree').Expression} */ (context.visit(block.expression)); const array = /** @type {import('estree').Expression} */ (context.visit(block.expression));
const transitive_dependencies = serialize_transitive_dependencies( const transitive_dependencies = serialize_transitive_dependencies(
@ -2362,15 +2394,24 @@ export const template_visitors = {
'$.invalidate_inner_signals', '$.invalidate_inner_signals',
b.thunk(b.sequence(indirect_dependencies)) b.thunk(b.sequence(indirect_dependencies))
); );
const invalidate_store = store_to_invalidate
? b.call('$.invalidate_store', b.id('$$subscriptions'), b.literal(store_to_invalidate))
: undefined;
const sequence = [];
if (!context.state.analysis.runes) sequence.push(invalidate);
if (invalidate_store) sequence.push(invalidate_store);
if (left === assignment.left) { if (left === assignment.left) {
const assign = b.assignment('=', expression_for_id, value); const assign = b.assignment('=', expression_for_id, value);
return context.state.analysis.runes ? assign : b.sequence([assign, invalidate]); sequence.unshift(assign);
return b.sequence(sequence);
} else { } else {
const original_left = /** @type {import('estree').MemberExpression} */ (assignment.left); const original_left = /** @type {import('estree').MemberExpression} */ (assignment.left);
const left = context.visit(original_left); const left = context.visit(original_left);
const assign = b.assignment(assignment.operator, left, value); const assign = b.assignment(assignment.operator, left, value);
return context.state.analysis.runes ? assign : b.sequence([assign, invalidate]); sequence.unshift(assign);
return b.sequence(sequence);
} }
}; };
}; };
@ -2385,7 +2426,7 @@ export const template_visitors = {
const binding = /** @type {import('#compiler').Binding} */ (context.state.scope.get(item.name)); const binding = /** @type {import('#compiler').Binding} */ (context.state.scope.get(item.name));
binding.expression = (id) => { binding.expression = (id) => {
const item_with_loc = with_loc(item, id); const item_with_loc = with_loc(item, id);
return each_item_is_reactive ? b.call('$.unwrap', item_with_loc) : item_with_loc; return b.call('$.unwrap', item_with_loc);
}; };
if (node.index) { if (node.index) {
const index_binding = /** @type {import('#compiler').Binding} */ ( const index_binding = /** @type {import('#compiler').Binding} */ (
@ -2393,7 +2434,7 @@ export const template_visitors = {
); );
index_binding.expression = (id) => { index_binding.expression = (id) => {
const index_with_loc = with_loc(index, id); const index_with_loc = with_loc(index, id);
return each_item_is_reactive ? b.call('$.unwrap', index_with_loc) : index_with_loc; return b.call('$.unwrap', index_with_loc);
}; };
} }
@ -2660,7 +2701,7 @@ export const template_visitors = {
const params = [b.id('$$node')]; const params = [b.id('$$node')];
if (node.expression) { if (node.expression) {
params.push(b.id('$$props')); params.push(b.id('$$action_arg'));
} }
/** @type {import('estree').Expression[]} */ /** @type {import('estree').Expression[]} */

@ -191,6 +191,8 @@ const visitors = {
next({ ...state, specificity }); next({ ...state, specificity });
}, },
ComplexSelector(node, context) { ComplexSelector(node, context) {
const before_bumped = context.state.specificity.bumped;
/** @param {import('#compiler').Css.SimpleSelector} selector */ /** @param {import('#compiler').Css.SimpleSelector} selector */
function remove_global_pseudo_class(selector) { function remove_global_pseudo_class(selector) {
context.state.code context.state.code
@ -261,6 +263,8 @@ const visitors = {
} }
context.next(); context.next();
context.state.specificity.bumped = before_bumped;
}, },
PseudoClassSelector(node, context) { PseudoClassSelector(node, context) {
if (node.name === 'is' || node.name === 'where') { if (node.name === 'is' || node.name === 'where') {

@ -1,6 +1,11 @@
import { walk } from 'zimmerframe'; import { walk } from 'zimmerframe';
import { set_scope, get_rune } from '../../scope.js'; import { set_scope, get_rune } from '../../scope.js';
import { extract_identifiers, extract_paths, is_event_attribute } from '../../../utils/ast.js'; import {
extract_identifiers,
extract_paths,
is_event_attribute,
unwrap_optional
} from '../../../utils/ast.js';
import * as b from '../../../utils/builders.js'; import * as b from '../../../utils/builders.js';
import is_reference from 'is-reference'; import is_reference from 'is-reference';
import { import {
@ -541,6 +546,113 @@ const javascript_visitors = {
/** @type {import('./types').Visitors} */ /** @type {import('./types').Visitors} */
const javascript_visitors_runes = { const javascript_visitors_runes = {
ClassBody(node, { state, visit }) {
/** @type {Map<string, import('../../3-transform/client/types.js').StateField>} */
const public_derived = new Map();
/** @type {Map<string, import('../../3-transform/client/types.js').StateField>} */
const private_derived = new Map();
/** @type {string[]} */
const private_ids = [];
for (const definition of node.body) {
if (
definition.type === 'PropertyDefinition' &&
(definition.key.type === 'Identifier' || definition.key.type === 'PrivateIdentifier')
) {
const { type, name } = definition.key;
const is_private = type === 'PrivateIdentifier';
if (is_private) private_ids.push(name);
if (definition.value?.type === 'CallExpression') {
const rune = get_rune(definition.value, state.scope);
if (rune === '$derived' || rune === '$derived.by') {
/** @type {import('../../3-transform/client/types.js').StateField} */
const field = {
kind: rune === '$derived.by' ? 'derived_call' : 'derived',
// @ts-expect-error this is set in the next pass
id: is_private ? definition.key : null
};
if (is_private) {
private_derived.set(name, field);
} else {
public_derived.set(name, field);
}
}
}
}
}
// each `foo = $derived()` needs a backing `#foo` field
for (const [name, field] of public_derived) {
let deconflicted = name;
while (private_ids.includes(deconflicted)) {
deconflicted = '_' + deconflicted;
}
private_ids.push(deconflicted);
field.id = b.private_id(deconflicted);
}
/** @type {Array<import('estree').MethodDefinition | import('estree').PropertyDefinition>} */
const body = [];
const child_state = { ...state, private_derived };
// Replace parts of the class body
for (const definition of node.body) {
if (
definition.type === 'PropertyDefinition' &&
(definition.key.type === 'Identifier' || definition.key.type === 'PrivateIdentifier')
) {
const name = definition.key.name;
const is_private = definition.key.type === 'PrivateIdentifier';
const field = (is_private ? private_derived : public_derived).get(name);
if (definition.value?.type === 'CallExpression' && field !== undefined) {
const init = /** @type {import('estree').Expression} **/ (
visit(definition.value.arguments[0], child_state)
);
const value =
field.kind === 'derived_call'
? b.call('$.once', init)
: b.call('$.once', b.thunk(init));
if (is_private) {
body.push(b.prop_def(field.id, value));
} else {
// #foo;
const member = b.member(b.this, field.id);
body.push(b.prop_def(field.id, value));
// get foo() { return this.#foo; }
body.push(b.method('get', definition.key, [], [b.return(b.call(member))]));
if ((field.kind === 'derived' || field.kind === 'derived_call') && state.options.dev) {
body.push(
b.method(
'set',
definition.key,
[b.id('_')],
[b.throw_error(`Cannot update a derived property ('${name}')`)]
)
);
}
}
continue;
}
}
body.push(/** @type {import('estree').MethodDefinition} **/ (visit(definition, child_state)));
}
return { ...node, body };
},
PropertyDefinition(node, { state, next, visit }) { PropertyDefinition(node, { state, next, visit }) {
if (node.value != null && node.value.type === 'CallExpression') { if (node.value != null && node.value.type === 'CallExpression') {
const rune = get_rune(node.value, state.scope); const rune = get_rune(node.value, state.scope);
@ -648,6 +760,16 @@ const javascript_visitors_runes = {
return transform_inspect_rune(node, context); return transform_inspect_rune(node, context);
} }
context.next();
},
MemberExpression(node, context) {
if (node.object.type === 'ThisExpression' && node.property.type === 'PrivateIdentifier') {
const field = context.state.private_derived.get(node.property.name);
if (field) {
return b.call(node);
}
}
context.next(); context.next();
} }
}; };
@ -1141,17 +1263,28 @@ const template_visitors = {
state.init.push(anchor); state.init.push(anchor);
state.template.push(t_expression(anchor_id)); state.template.push(t_expression(anchor_id));
const expression = /** @type {import('estree').Expression} */ (context.visit(node.expression)); const callee = unwrap_optional(node.expression).callee;
const raw_args = unwrap_optional(node.expression).arguments;
const expression = /** @type {import('estree').Expression} */ (context.visit(callee));
const snippet_function = state.options.dev const snippet_function = state.options.dev
? b.call('$.validate_snippet', expression) ? b.call('$.validate_snippet', expression)
: expression; : expression;
const snippet_args = node.arguments.map((arg) => { const snippet_args = raw_args.map((arg) => {
return /** @type {import('estree').Expression} */ (context.visit(arg)); return /** @type {import('estree').Expression} */ (context.visit(arg));
}); });
state.template.push( state.template.push(
t_statement(b.stmt(b.call(snippet_function, b.id('$$payload'), ...snippet_args))) t_statement(
b.stmt(
(node.expression.type === 'CallExpression' ? b.call : b.maybe_call)(
snippet_function,
b.id('$$payload'),
...snippet_args
)
)
)
); );
state.template.push(t_expression(anchor_id)); state.template.push(t_expression(anchor_id));
@ -1207,6 +1340,12 @@ const template_visitors = {
inner_context.visit(node, state); inner_context.visit(node, state);
} }
if (context.state.options.dev) {
context.state.template.push(
t_statement(b.stmt(b.call('$.push_element', b.literal(node.name), b.id('$$payload'))))
);
}
process_children(trimmed, node, inner_context); process_children(trimmed, node, inner_context);
if (body_expression !== null) { if (body_expression !== null) {
@ -1239,6 +1378,9 @@ const template_visitors = {
if (!VoidElements.includes(node.name) && metadata.namespace !== 'foreign') { if (!VoidElements.includes(node.name) && metadata.namespace !== 'foreign') {
context.state.template.push(t_string(`</${node.name}>`)); context.state.template.push(t_string(`</${node.name}>`));
} }
if (context.state.options.dev) {
context.state.template.push(t_statement(b.stmt(b.call('$.pop_element'))));
}
}, },
SvelteElement(node, context) { SvelteElement(node, context) {
let tag = /** @type {import('estree').Expression} */ (context.visit(node.tag)); let tag = /** @type {import('estree').Expression} */ (context.visit(node.tag));
@ -1274,40 +1416,45 @@ const template_visitors = {
context.state.init.push(el_anchor); context.state.init.push(el_anchor);
context.state.template.push(t_expression(anchor_id)); context.state.template.push(t_expression(anchor_id));
const [inner_anchor, inner_id] = serialize_anchor(context.state);
inner_context.state.init.push(inner_anchor);
inner_context.state.template.push(t_string('<'), t_expression(tag));
serialize_element_attributes(node, inner_context);
inner_context.state.template.push(t_string('>'));
const before = serialize_template(inner_context.state.template);
const main = create_block(node, node.fragment.nodes, { const main = create_block(node, node.fragment.nodes, {
...context, ...context,
state: { ...context.state, metadata } state: { ...context.state, metadata }
}); });
const after = serialize_template([
t_expression(inner_id), serialize_element_attributes(node, inner_context);
t_string('</'),
t_expression(tag), if (context.state.options.dev) {
t_string('>') context.state.template.push(
]); t_statement(b.stmt(b.call('$.push_element', tag, b.id('$$payload'))))
);
}
context.state.template.push( context.state.template.push(
t_statement( t_statement(
b.if( b.if(
tag, tag,
b.block([
...inner_context.state.init, b.stmt(
...before, b.call(
b.if( '$.element',
b.unary('!', b.call('$.VoidElements.has', tag)), b.id('$$payload'),
b.block([...serialize_template([t_expression(inner_id)]), ...main, ...after]) tag,
b.thunk(
b.block([
...inner_context.state.init,
...serialize_template(inner_context.state.template)
])
),
b.thunk(b.block(main))
) )
]) )
) )
), ),
t_expression(anchor_id) t_expression(anchor_id)
); );
if (context.state.options.dev) {
context.state.template.push(t_statement(b.stmt(b.call('$.pop_element'))));
}
}, },
EachBlock(node, context) { EachBlock(node, context) {
const state = context.state; const state = context.state;
@ -1982,7 +2129,8 @@ export function server_component(analysis, options) {
metadata: { metadata: {
namespace: options.namespace namespace: options.namespace
}, },
preserve_whitespace: options.preserveWhitespace preserve_whitespace: options.preserveWhitespace,
private_derived: new Map()
}; };
const module = /** @type {import('estree').Program} */ ( const module = /** @type {import('estree').Program} */ (
@ -2239,7 +2387,8 @@ export function server_module(analysis, options) {
// this is an anomaly — it can only be used in components, but it needs // this is an anomaly — it can only be used in components, but it needs
// to be present for `javascript_visitors` and so is included in module // to be present for `javascript_visitors` and so is included in module
// transform state as well as component transform state // transform state as well as component transform state
legacy_reactive_statements: new Map() legacy_reactive_statements: new Map(),
private_derived: new Map()
}; };
const module = /** @type {import('estree').Program} */ ( const module = /** @type {import('estree').Program} */ (

@ -8,6 +8,7 @@ import type {
import type { SvelteNode, Namespace, ValidatedCompileOptions } from '#compiler'; import type { SvelteNode, Namespace, ValidatedCompileOptions } from '#compiler';
import type { TransformState } from '../types.js'; import type { TransformState } from '../types.js';
import type { ComponentAnalysis } from '../../types.js'; import type { ComponentAnalysis } from '../../types.js';
import type { StateField } from '../client/types.js';
export type TemplateExpression = { export type TemplateExpression = {
type: 'expression'; type: 'expression';
@ -35,6 +36,7 @@ export interface Anchor {
export interface ServerTransformState extends TransformState { export interface ServerTransformState extends TransformState {
/** The $: calls, which will be ordered in the end */ /** The $: calls, which will be ordered in the end */
readonly legacy_reactive_statements: Map<LabeledStatement, Statement>; readonly legacy_reactive_statements: Map<LabeledStatement, Statement>;
readonly private_derived: Map<string, StateField>;
} }
export interface ComponentServerTransformState extends ServerTransformState { export interface ComponentServerTransformState extends ServerTransformState {

@ -1,10 +1,10 @@
import { import {
regex_ends_with_whitespaces, regex_ends_with_whitespaces,
regex_not_whitespace, regex_not_whitespace,
regex_starts_with_whitespaces, regex_starts_with_whitespaces
regex_whitespaces_strict
} from '../patterns.js'; } from '../patterns.js';
import * as b from '../../utils/builders.js'; import * as b from '../../utils/builders.js';
import { walk } from 'zimmerframe';
/** /**
* @param {string} s * @param {string} s
@ -162,24 +162,36 @@ export function clean_nodes(
/** @type {import('#compiler').SvelteNode[]} */ /** @type {import('#compiler').SvelteNode[]} */
const trimmed = []; const trimmed = [];
/** @type {import('#compiler').Text | null} */ // Replace any whitespace between a text and non-text node with a single spaceand keep whitespace
let last_text = null; // as-is within text nodes, or between text nodes and expression tags (because in the end they count
// as one text). This way whitespace is mostly preserved when using CSS with `white-space: pre-line`
// and default slot content going into a pre tag (which we can't see).
for (let i = 0; i < regular.length; i++) {
const prev = regular[i - 1];
const node = regular[i];
const next = regular[i + 1];
// Replace any inbetween whitespace with a single space
for (const node of regular) {
if (node.type === 'Text') { if (node.type === 'Text') {
node.data = node.data.replace(regex_whitespaces_strict, ' '); if (prev?.type !== 'ExpressionTag') {
node.raw = node.raw.replace(regex_whitespaces_strict, ' '); const prev_is_text_ending_with_whitespace =
if ( prev?.type === 'Text' && regex_ends_with_whitespaces.test(prev.data);
(last_text === null && !can_remove_entirely) || node.data = node.data.replace(
node.data !== ' ' || regex_starts_with_whitespaces,
node.data.charCodeAt(0) === 160 // non-breaking space prev_is_text_ending_with_whitespace ? '' : ' '
) { );
node.raw = node.raw.replace(
regex_starts_with_whitespaces,
prev_is_text_ending_with_whitespace ? '' : ' '
);
}
if (next?.type !== 'ExpressionTag') {
node.data = node.data.replace(regex_ends_with_whitespaces, ' ');
node.raw = node.raw.replace(regex_ends_with_whitespaces, ' ');
}
if (node.data && (node.data !== ' ' || !can_remove_entirely)) {
trimmed.push(node); trimmed.push(node);
} }
last_text = node;
} else { } else {
last_text = null;
trimmed.push(node); trimmed.push(node);
} }
} }
@ -237,51 +249,49 @@ export function infer_namespace(namespace, parent, nodes, path) {
* @param {import('#compiler').Namespace | 'keep' | 'maybe_html'} namespace * @param {import('#compiler').Namespace | 'keep' | 'maybe_html'} namespace
*/ */
function check_nodes_for_namespace(nodes, namespace) { function check_nodes_for_namespace(nodes, namespace) {
/**
* @param {import('#compiler').SvelteElement | import('#compiler').RegularElement} node}
* @param {{stop: () => void}} context
*/
const RegularElement = (node, { stop }) => {
if (!node.metadata.svg) {
namespace = 'html';
stop();
} else if (namespace === 'keep') {
namespace = 'svg';
}
};
for (const node of nodes) { for (const node of nodes) {
if (node.type === 'RegularElement' || node.type === 'SvelteElement') { walk(
if (!node.metadata.svg) { node,
namespace = 'html'; {},
break; {
} else if (namespace === 'keep') { _(node, { next }) {
namespace = 'svg'; if (
} node.type === 'EachBlock' ||
} else if ( node.type === 'IfBlock' ||
(node.type === 'Text' && node.data.trim() !== '') || node.type === 'AwaitBlock' ||
node.type === 'HtmlTag' || node.type === 'Fragment' ||
node.type === 'RenderTag' node.type === 'KeyBlock' ||
) { node.type === 'RegularElement' ||
namespace = 'maybe_html'; node.type === 'SvelteElement' ||
} else if (node.type === 'EachBlock') { node.type === 'Text'
namespace = check_nodes_for_namespace(node.body.nodes, namespace); ) {
if (namespace === 'html') break; next();
if (node.fallback) { }
namespace = check_nodes_for_namespace(node.fallback.nodes, namespace); },
if (namespace === 'html') break; SvelteElement: RegularElement,
} RegularElement,
} else if (node.type === 'IfBlock') { Text(node) {
namespace = check_nodes_for_namespace(node.consequent.nodes, namespace); if (node.data.trim() !== '') {
if (namespace === 'html') break; namespace = 'maybe_html';
if (node.alternate) { }
namespace = check_nodes_for_namespace(node.alternate.nodes, namespace); }
if (namespace === 'html') break;
} }
} else if (node.type === 'AwaitBlock') { );
if (node.pending) {
namespace = check_nodes_for_namespace(node.pending.nodes, namespace); if (namespace === 'html') return namespace;
if (namespace === 'html') break;
}
if (node.then) {
namespace = check_nodes_for_namespace(node.then.nodes, namespace);
if (namespace === 'html') break;
}
if (node.catch) {
namespace = check_nodes_for_namespace(node.catch.nodes, namespace);
if (namespace === 'html') break;
}
} else if (node.type === 'KeyBlock') {
namespace = check_nodes_for_namespace(node.fragment.nodes, namespace);
if (namespace === 'html') break;
}
} }
return namespace; return namespace;

@ -2,9 +2,9 @@ export const regex_whitespace = /\s/;
export const regex_whitespaces = /\s+/; export const regex_whitespaces = /\s+/;
export const regex_starts_with_newline = /^\r?\n/; export const regex_starts_with_newline = /^\r?\n/;
export const regex_starts_with_whitespace = /^\s/; export const regex_starts_with_whitespace = /^\s/;
export const regex_starts_with_whitespaces = /^[ \t\r\n]*/; export const regex_starts_with_whitespaces = /^[ \t\r\n]+/;
export const regex_ends_with_whitespace = /\s$/; export const regex_ends_with_whitespace = /\s$/;
export const regex_ends_with_whitespaces = /[ \t\r\n]*$/; export const regex_ends_with_whitespaces = /[ \t\r\n]+$/;
/** Not \S because that also removes explicit whitespace defined through things like `&nbsp;` */ /** Not \S because that also removes explicit whitespace defined through things like `&nbsp;` */
export const regex_not_whitespace = /[^ \t\r\n]/; export const regex_not_whitespace = /[^ \t\r\n]/;
/** Not \s+ because that also includes explicit whitespace defined through things like `&nbsp;` */ /** Not \s+ because that also includes explicit whitespace defined through things like `&nbsp;` */

@ -14,7 +14,7 @@ export class Scope {
* The immediate parent scope * The immediate parent scope
* @type {Scope | null} * @type {Scope | null}
*/ */
#parent; parent;
/** /**
* Whether or not `var` declarations are contained by this scope * Whether or not `var` declarations are contained by this scope
@ -56,7 +56,7 @@ export class Scope {
*/ */
constructor(root, parent, porous) { constructor(root, parent, porous) {
this.root = root; this.root = root;
this.#parent = parent; this.parent = parent;
this.#porous = porous; this.#porous = porous;
this.function_depth = parent ? parent.function_depth + (porous ? 0 : 1) : 0; this.function_depth = parent ? parent.function_depth + (porous ? 0 : 1) : 0;
} }
@ -83,13 +83,13 @@ export class Scope {
error(node, 'invalid-dollar-prefix'); error(node, 'invalid-dollar-prefix');
} }
if (this.#parent) { if (this.parent) {
if (declaration_kind === 'var' && this.#porous) { if (declaration_kind === 'var' && this.#porous) {
return this.#parent.declare(node, kind, declaration_kind); return this.parent.declare(node, kind, declaration_kind);
} }
if (declaration_kind === 'import') { if (declaration_kind === 'import') {
return this.#parent.declare(node, kind, declaration_kind, initial); return this.parent.declare(node, kind, declaration_kind, initial);
} }
} }
@ -112,7 +112,8 @@ export class Scope {
prop_alias: null, prop_alias: null,
expression: null, expression: null,
mutation: null, mutation: null,
reassigned: false reassigned: false,
metadata: null
}; };
this.declarations.set(node.name, binding); this.declarations.set(node.name, binding);
this.root.conflicts.add(node.name); this.root.conflicts.add(node.name);
@ -129,7 +130,7 @@ export class Scope {
*/ */
generate(preferred_name) { generate(preferred_name) {
if (this.#porous) { if (this.#porous) {
return /** @type {Scope} */ (this.#parent).generate(preferred_name); return /** @type {Scope} */ (this.parent).generate(preferred_name);
} }
preferred_name = preferred_name.replace(/[^a-zA-Z0-9_$]/g, '_').replace(/^[0-9]/, '_'); preferred_name = preferred_name.replace(/[^a-zA-Z0-9_$]/g, '_').replace(/^[0-9]/, '_');
@ -155,7 +156,7 @@ export class Scope {
* @returns {import('#compiler').Binding | null} * @returns {import('#compiler').Binding | null}
*/ */
get(name) { get(name) {
return this.declarations.get(name) ?? this.#parent?.get(name) ?? null; return this.declarations.get(name) ?? this.parent?.get(name) ?? null;
} }
/** /**
@ -175,7 +176,7 @@ export class Scope {
* @returns {Scope | null} * @returns {Scope | null}
*/ */
owner(name) { owner(name) {
return this.declarations.has(name) ? this : this.#parent && this.#parent.owner(name); return this.declarations.has(name) ? this : this.parent && this.parent.owner(name);
} }
/** /**
@ -193,8 +194,8 @@ export class Scope {
const binding = this.declarations.get(node.name); const binding = this.declarations.get(node.name);
if (binding) { if (binding) {
binding.references.push({ node, path }); binding.references.push({ node, path });
} else if (this.#parent) { } else if (this.parent) {
this.#parent.reference(node, path); this.parent.reference(node, path);
} else { } else {
// no binding was found, and this is the top level scope, // no binding was found, and this is the top level scope,
// which means this is a global // which means this is a global
@ -534,11 +535,31 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
// declarations // declarations
for (const id of extract_identifiers(node.context)) { for (const id of extract_identifiers(node.context)) {
scope.declare(id, 'each', 'const'); const binding = scope.declare(id, 'each', 'const');
let inside_rest = false;
let is_rest_id = false;
walk(node.context, null, {
Identifier(node) {
if (inside_rest && node === id) {
is_rest_id = true;
}
},
RestElement(_, { next }) {
const prev = inside_rest;
inside_rest = true;
next();
inside_rest = prev;
}
});
binding.metadata = { inside_rest: is_rest_id };
} }
if (node.context.type !== 'Identifier') { if (node.context.type !== 'Identifier') {
scope.declare(b.id('$$item'), 'derived', 'synthetic'); scope.declare(b.id('$$item'), 'derived', 'synthetic');
} }
// Visit to pick up references from default initializers
visit(node.context, { scope });
if (node.index) { if (node.index) {
const is_keyed = const is_keyed =

@ -11,7 +11,7 @@ import type { SourceMap } from 'magic-string';
import type { Context } from 'zimmerframe'; import type { Context } from 'zimmerframe';
import type { Scope } from '../phases/scope.js'; import type { Scope } from '../phases/scope.js';
import * as Css from './css.js'; import * as Css from './css.js';
import type { EachBlock, Namespace, SvelteNode } from './template.js'; import type { EachBlock, Namespace, SvelteNode, SvelteOptions } from './template.js';
/** The return value of `compile` from `svelte/compiler` */ /** The return value of `compile` from `svelte/compiler` */
export interface CompileResult { export interface CompileResult {
@ -224,6 +224,7 @@ export type ValidatedCompileOptions = ValidatedModuleCompileOptions &
sourcemap: CompileOptions['sourcemap']; sourcemap: CompileOptions['sourcemap'];
legacy: Required<Required<CompileOptions>['legacy']>; legacy: Required<Required<CompileOptions>['legacy']>;
runes: CompileOptions['runes']; runes: CompileOptions['runes'];
customElementOptions: SvelteOptions['customElement'];
}; };
export type DeclarationKind = export type DeclarationKind =
@ -288,6 +289,11 @@ export interface Binding {
expression: Expression | ((id: Identifier) => Expression) | null; expression: Expression | ((id: Identifier) => Expression) | null;
/** If this is set, all mutations should use this expression */ /** If this is set, all mutations should use this expression */
mutation: ((assignment: AssignmentExpression, context: Context<any, any>) => Expression) | null; mutation: ((assignment: AssignmentExpression, context: Context<any, any>) => Expression) | null;
/** Additional metadata, varies per binding type */
metadata: {
/** `true` if is (inside) a rest parameter */
inside_rest?: boolean;
} | null;
} }
export * from './template.js'; export * from './template.js';

@ -13,9 +13,9 @@ import type {
ObjectExpression, ObjectExpression,
Pattern, Pattern,
Program, Program,
SpreadElement ChainExpression,
SimpleCallExpression
} from 'estree'; } from 'estree';
import type { Atrule, Rule } from './css';
export interface BaseNode { export interface BaseNode {
type: string; type: string;
@ -65,7 +65,7 @@ export interface Root extends BaseNode {
} }
export interface SvelteOptions { export interface SvelteOptions {
// start/end info (needed for Prettier, when someone wants to keep the options where they are) // start/end info (needed for warnings and for our Prettier plugin)
start: number; start: number;
end: number; end: number;
// options // options
@ -151,8 +151,7 @@ export interface DebugTag extends BaseNode {
/** A `{@render foo(...)} tag */ /** A `{@render foo(...)} tag */
export interface RenderTag extends BaseNode { export interface RenderTag extends BaseNode {
type: 'RenderTag'; type: 'RenderTag';
expression: Identifier; expression: SimpleCallExpression | (ChainExpression & { expression: SimpleCallExpression });
arguments: Array<Expression | SpreadElement>;
} }
type Tag = ExpressionTag | HtmlTag | ConstTag | DebugTag | RenderTag; type Tag = ExpressionTag | HtmlTag | ConstTag | DebugTag | RenderTag;

@ -361,3 +361,12 @@ export function is_simple_expression(node) {
return false; return false;
} }
/**
* @template {import('estree').SimpleCallExpression | import('estree').MemberExpression} T
* @param {import('estree').ChainExpression & { expression : T } | T} node
* @returns {T}
*/
export function unwrap_optional(node) {
return node.type === 'ChainExpression' ? node.expression : node;
}

@ -15,7 +15,15 @@ const attributes = {
'avoid-is': () => 'The "is" attribute is not supported cross-browser and should be avoided', 'avoid-is': () => 'The "is" attribute is not supported cross-browser and should be avoided',
/** @param {string} name */ /** @param {string} name */
'global-event-reference': (name) => 'global-event-reference': (name) =>
`You are referencing globalThis.${name}. Did you forget to declare a variable with that name?` `You are referencing globalThis.${name}. Did you forget to declare a variable with that name?`,
'illegal-attribute-character': () =>
"Attributes should not contain ':' characters to prevent ambiguity with Svelte directives",
/**
* @param {string} wrong
* @param {string} right
*/
'invalid-html-attribute': (wrong, right) =>
`'${wrong}' is not a valid HTML attribute. Did you mean '${right}'?`
}; };
/** @satisfies {Warnings} */ /** @satisfies {Warnings} */
@ -27,7 +35,10 @@ const runes = {
/** @param {string} name */ /** @param {string} name */
'non-state-reference': (name) => 'non-state-reference': (name) =>
`${name} is updated, but is not declared with $state(...). Changing its value will not correctly trigger updates.`, `${name} is updated, but is not declared with $state(...). Changing its value will not correctly trigger updates.`,
'derived-iife': () => `Use \`$derived.by(() => {...})\` instead of \`$derived((() => {...})());\`` 'derived-iife': () =>
`Use \`$derived.by(() => {...})\` instead of \`$derived((() => {...})());\``,
'invalid-props-declaration': () =>
`Component properties are declared using $props() in runes mode. Did you forget to call the function?`
}; };
/** @satisfies {Warnings} */ /** @satisfies {Warnings} */
@ -192,7 +203,10 @@ const a11y = {
/** @satisfies {Warnings} */ /** @satisfies {Warnings} */
const state = { const state = {
'static-state-reference': () => 'static-state-reference': () =>
`State referenced in its own scope will never update. Did you mean to reference it inside a closure?` `State referenced in its own scope will never update. Did you mean to reference it inside a closure?`,
/** @param {string} name */
'invalid-rest-eachblock-binding': (name) =>
`The rest operator (...) will create a new object and binding '${name}' with the original object will not work`
}; };
/** @satisfies {Warnings} */ /** @satisfies {Warnings} */
@ -211,7 +225,21 @@ const components = {
const legacy = { const legacy = {
'no-reactive-declaration': () => 'no-reactive-declaration': () =>
`Reactive declarations only exist at the top level of the instance script` `Reactive declarations only exist at the top level of the instance script`,
'module-script-reactive-declaration': () =>
'All dependencies of the reactive declaration are declared in a module script and will not be reactive',
/** @param {string} name */
'unused-export-let': (name) =>
`Component has unused export property '${name}'. If it is for external reference only, please consider using \`export const ${name}\``
};
const block = {
'empty-block': () => 'Empty block'
};
const options = {
'missing-custom-element-compile-option': () =>
"The 'customElement' option is used when generating a custom element. Did you forget the 'customElement: true' compile option?"
}; };
/** @satisfies {Warnings} */ /** @satisfies {Warnings} */
@ -223,7 +251,9 @@ const warnings = {
...performance, ...performance,
...state, ...state,
...components, ...components,
...legacy ...legacy,
...block,
...options
}; };
/** @typedef {typeof warnings} AllWarnings */ /** @typedef {typeof warnings} AllWarnings */
@ -231,7 +261,7 @@ const warnings = {
/** /**
* @template {keyof AllWarnings} T * @template {keyof AllWarnings} T
* @param {import('./phases/types').RawWarning[]} array the array to push the warning to, if not ignored * @param {import('./phases/types').RawWarning[]} array the array to push the warning to, if not ignored
* @param {{ start?: number, end?: number, parent?: import('#compiler').SvelteNode | null, leadingComments?: import('estree').Comment[] } | null} node the node related to the warning * @param {{ start?: number, end?: number, type?: string, parent?: import('#compiler').SvelteNode | null, leadingComments?: import('estree').Comment[] } | null} node the node related to the warning
* @param {import('#compiler').SvelteNode[]} path the path to the node, so that we can traverse upwards to find svelte-ignore comments * @param {import('#compiler').SvelteNode[]} path the path to the node, so that we can traverse upwards to find svelte-ignore comments
* @param {T} code the warning code * @param {T} code the warning code
* @param {Parameters<AllWarnings[T]>} args the arguments to pass to the warning function * @param {Parameters<AllWarnings[T]>} args the arguments to pass to the warning function
@ -246,6 +276,13 @@ export function warn(array, node, path, code, ...args) {
/** @type {string[]} */ /** @type {string[]} */
const ignores = []; const ignores = [];
if (node) {
// comments inside JavaScript (estree)
if ('leadingComments' in node) {
ignores.push(...extract_svelte_ignore_from_comments(node));
}
}
for (let i = path.length - 1; i >= 0; i--) { for (let i = path.length - 1; i >= 0; i--) {
const current = path[i]; const current = path[i];

@ -88,3 +88,164 @@ export const DOMBooleanAttributes = [
export const namespace_svg = 'http://www.w3.org/2000/svg'; export const namespace_svg = 'http://www.w3.org/2000/svg';
export const namespace_html = 'http://www.w3.org/1999/xhtml'; export const namespace_html = 'http://www.w3.org/1999/xhtml';
// while `input` is also an interactive element, it is never moved by the browser, so we don't need to check for it
export const interactive_elements = new Set([
'a',
'button',
'iframe',
'embed',
'select',
'textarea'
]);
export const disallowed_paragraph_contents = [
'address',
'article',
'aside',
'blockquote',
'details',
'div',
'dl',
'fieldset',
'figcapture',
'figure',
'footer',
'form',
'h1',
'h2',
'h3',
'h4',
'h5',
'h6',
'header',
'hr',
'menu',
'nav',
'ol',
'pre',
'section',
'table',
'ul'
];
// https://html.spec.whatwg.org/multipage/syntax.html#generate-implied-end-tags
const implied_end_tags = ['dd', 'dt', 'li', 'option', 'optgroup', 'p', 'rp', 'rt'];
/**
* @param {string} tag
* @param {string} parent_tag
* @returns {boolean}
*/
export function is_tag_valid_with_parent(tag, parent_tag) {
// First, let's check if we're in an unusual parsing mode...
switch (parent_tag) {
// https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-inselect
case 'select':
return tag === 'option' || tag === 'optgroup' || tag === '#text';
case 'optgroup':
return tag === 'option' || tag === '#text';
// Strictly speaking, seeing an <option> doesn't mean we're in a <select>
// but
case 'option':
return tag === '#text';
// https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-intd
// https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-incaption
// No special behavior since these rules fall back to "in body" mode for
// all except special table nodes which cause bad parsing behavior anyway.
// https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-intr
case 'tr':
return (
tag === 'th' || tag === 'td' || tag === 'style' || tag === 'script' || tag === 'template'
);
// https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-intbody
case 'tbody':
case 'thead':
case 'tfoot':
return tag === 'tr' || tag === 'style' || tag === 'script' || tag === 'template';
// https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-incolgroup
case 'colgroup':
return tag === 'col' || tag === 'template';
// https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-intable
case 'table':
return (
tag === 'caption' ||
tag === 'colgroup' ||
tag === 'tbody' ||
tag === 'tfoot' ||
tag === 'thead' ||
tag === 'style' ||
tag === 'script' ||
tag === 'template'
);
// https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-inhead
case 'head':
return (
tag === 'base' ||
tag === 'basefont' ||
tag === 'bgsound' ||
tag === 'link' ||
tag === 'meta' ||
tag === 'title' ||
tag === 'noscript' ||
tag === 'noframes' ||
tag === 'style' ||
tag === 'script' ||
tag === 'template'
);
// https://html.spec.whatwg.org/multipage/semantics.html#the-html-element
case 'html':
return tag === 'head' || tag === 'body' || tag === 'frameset';
case 'frameset':
return tag === 'frame';
case '#document':
return tag === 'html';
}
// Probably in the "in body" parsing mode, so we outlaw only tag combos
// where the parsing rules cause implicit opens or closes to be added.
// https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-inbody
switch (tag) {
case 'h1':
case 'h2':
case 'h3':
case 'h4':
case 'h5':
case 'h6':
return (
parent_tag !== 'h1' &&
parent_tag !== 'h2' &&
parent_tag !== 'h3' &&
parent_tag !== 'h4' &&
parent_tag !== 'h5' &&
parent_tag !== 'h6'
);
case 'rp':
case 'rt':
return implied_end_tags.indexOf(parent_tag) === -1;
case 'body':
case 'caption':
case 'col':
case 'colgroup':
case 'frameset':
case 'frame':
case 'head':
case 'html':
case 'tbody':
case 'td':
case 'tfoot':
case 'th':
case 'thead':
case 'tr':
// These tags are only valid with a few parents that have special child
// parsing rules -- if we're down here, then none of those matched and
// so we allow it only if we don't know what the parent is, as all other
// cases are invalid.
return parent_tag == null;
}
return true;
}

@ -1,93 +0,0 @@
import {
ROOT_BLOCK,
HEAD_BLOCK,
DYNAMIC_ELEMENT_BLOCK,
DYNAMIC_COMPONENT_BLOCK,
SNIPPET_BLOCK
} from './constants.js';
import { current_block } from './runtime.js';
/**
* @param {boolean} intro
* @returns {import('./types.js').RootBlock}
*/
export function create_root_block(intro) {
return {
// dom
d: null,
// effect
e: null,
// intro
i: intro,
// parent
p: null,
// transition
r: null,
// type
t: ROOT_BLOCK
};
}
/** @returns {import('./types.js').HeadBlock} */
export function create_head_block() {
return {
// dom
d: null,
// effect
e: null,
// parent
p: /** @type {import('./types.js').Block} */ (current_block),
// transition
r: null,
// type
t: HEAD_BLOCK
};
}
/** @returns {import('./types.js').DynamicElementBlock} */
export function create_dynamic_element_block() {
return {
// dom
d: null,
// effect
e: null,
// parent
p: /** @type {import('./types.js').Block} */ (current_block),
// transition
r: null,
// type
t: DYNAMIC_ELEMENT_BLOCK
};
}
/** @returns {import('./types.js').DynamicComponentBlock} */
export function create_dynamic_component_block() {
return {
// dom
d: null,
// effect
e: null,
// parent
p: /** @type {import('./types.js').Block} */ (current_block),
// transition
r: null,
// type
t: DYNAMIC_COMPONENT_BLOCK
};
}
/** @returns {import('./types.js').SnippetBlock} */
export function create_snippet_block() {
return {
// dom
d: null,
// parent
p: /** @type {import('./types.js').Block} */ (current_block),
// effect
e: null,
// transition
r: null,
// type
t: SNIPPET_BLOCK
};
}

@ -1,4 +1,3 @@
export const SOURCE = 1;
export const DERIVED = 1 << 1; export const DERIVED = 1 << 1;
export const EFFECT = 1 << 2; export const EFFECT = 1 << 2;
export const PRE_EFFECT = 1 << 3; export const PRE_EFFECT = 1 << 3;

@ -34,13 +34,24 @@ export function get_stack() {
* Determines which `.svelte` component is responsible for a given state change * Determines which `.svelte` component is responsible for a given state change
* @returns {Function | null} * @returns {Function | null}
*/ */
export function get_component() { function get_component() {
const stack = get_stack(); // 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; if (!stack) return null;
for (const entry of stack) { for (let i = 0; i < stack.length; i++) {
const entry = stack[i];
const modules = boundaries[entry.file]; const modules = boundaries[entry.file];
if (!modules) continue; 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) { for (const module of modules) {
if (module.start.line < entry.line && module.end.line > entry.line) { if (module.start.line < entry.line && module.end.line > entry.line) {
@ -75,8 +86,8 @@ export function mark_module_end() {
const end = get_stack()?.[2]; const end = get_stack()?.[2];
if (end) { if (end) {
// @ts-expect-error const boundaries_file = boundaries[end.file];
boundaries[end.file].at(-1).end = end; boundaries_file[boundaries_file.length - 1].end = end;
} }
} }

@ -1,15 +1,9 @@
import { is_promise } from '../../../common.js'; import { is_promise } from '../../../common.js';
import { hydrate_block_anchor } from '../../hydration.js'; import { hydrate_block_anchor } from '../hydration.js';
import { remove } from '../../reconciler.js'; import { remove } from '../reconciler.js';
import { import { current_block, execute_effect, flushSync } from '../../runtime.js';
current_block, import { destroy_effect, render_effect } from '../../reactivity/effects.js';
destroy_signal, import { trigger_transitions } from '../elements/transitions.js';
execute_effect,
flushSync,
push_destroy_fn
} from '../../runtime.js';
import { render_effect } from '../../reactivity/computations.js';
import { trigger_transitions } from '../../transitions.js';
import { AWAIT_BLOCK, UNINITIALIZED } from '../../constants.js'; import { AWAIT_BLOCK, UNINITIALIZED } from '../../constants.js';
/** @returns {import('../../types.js').AwaitBlock} */ /** @returns {import('../../types.js').AwaitBlock} */
@ -74,7 +68,7 @@ export function await_block(anchor_node, input, pending_fn, then_fn, catch_fn) {
remove(render.d); remove(render.d);
render.d = null; render.d = null;
} }
destroy_signal(render.e); destroy_effect(render.e);
render.e = null; render.e = null;
} }
} }
@ -181,7 +175,7 @@ export function await_block(anchor_node, input, pending_fn, then_fn, catch_fn) {
block, block,
false false
); );
push_destroy_fn(await_effect, () => { await_effect.ondestroy = () => {
let render = current_render; let render = current_render;
latest_token = {}; latest_token = {};
while (render !== null) { while (render !== null) {
@ -191,10 +185,10 @@ export function await_block(anchor_node, input, pending_fn, then_fn, catch_fn) {
} }
const effect = render.e; const effect = render.e;
if (effect !== null) { if (effect !== null) {
destroy_signal(effect); destroy_effect(effect);
} }
render = render.p; render = render.p;
} }
}); };
block.e = await_effect; block.e = await_effect;
} }

@ -0,0 +1,65 @@
import { namespace_svg } from '../../../../constants.js';
import { current_hydration_fragment, hydrate_block_anchor, hydrating } from '../hydration.js';
import { empty } from '../operations.js';
import { render_effect } from '../../reactivity/effects.js';
import { insert, remove } from '../reconciler.js';
/**
* @param {Element | Text | Comment} anchor
* @param {boolean} is_html
* @param {() => Record<string, string>} props
* @param {(anchor: Element | Text | Comment) => any} component
* @returns {void}
*/
export function css_props(anchor, is_html, props, component) {
hydrate_block_anchor(anchor);
/** @type {HTMLElement | SVGElement} */
let tag;
/** @type {Text | Comment} */
let component_anchor;
if (hydrating) {
// Hydration: css props element is surrounded by a ssr comment ...
tag = /** @type {HTMLElement | SVGElement} */ (current_hydration_fragment[0]);
// ... and the child(ren) of the css props element is also surround by a ssr comment
component_anchor = /** @type {Comment} */ (tag.firstChild);
} else {
if (is_html) {
tag = document.createElement('div');
tag.style.display = 'contents';
} else {
tag = document.createElementNS(namespace_svg, 'g');
}
insert(tag, null, anchor);
component_anchor = empty();
tag.appendChild(component_anchor);
}
component(component_anchor);
/** @type {Record<string, string>} */
let current_props = {};
const effect = render_effect(() => {
const next_props = props();
for (const key in current_props) {
if (!(key in next_props)) {
tag.style.removeProperty(key);
}
}
for (const key in next_props) {
tag.style.setProperty(key, next_props[key]);
}
current_props = next_props;
});
effect.ondestroy = () => {
remove(tag);
};
}

@ -13,21 +13,15 @@ import {
hydrate_block_anchor, hydrate_block_anchor,
hydrating, hydrating,
set_current_hydration_fragment set_current_hydration_fragment
} from '../../hydration.js'; } from '../hydration.js';
import { clear_text_content, empty, map_get, map_set } from '../../operations.js'; import { clear_text_content, empty } from '../operations.js';
import { insert, remove } from '../../reconciler.js'; import { insert, remove } from '../reconciler.js';
import { import { current_block, execute_effect } from '../../runtime.js';
current_block, import { destroy_effect, render_effect } from '../../reactivity/effects.js';
destroy_signal, import { source, mutable_source, set } from '../../reactivity/sources.js';
execute_effect, import { trigger_transitions } from '../elements/transitions.js';
push_destroy_fn, import { is_array, is_frozen, map_get, map_set } from '../../utils.js';
set_signal_value import { EACH_BLOCK, EACH_ITEM_BLOCK, STATE_SYMBOL } from '../../constants.js';
} from '../../runtime.js';
import { render_effect } from '../../reactivity/computations.js';
import { source, mutable_source } from '../../reactivity/sources.js';
import { trigger_transitions } from '../../transitions.js';
import { is_array } from '../../utils.js';
import { EACH_BLOCK, EACH_ITEM_BLOCK } from '../../constants.js';
const NEW_BLOCK = -1; const NEW_BLOCK = -1;
const MOVED_BLOCK = 99999999; const MOVED_BLOCK = 99999999;
@ -61,8 +55,8 @@ export function create_each_block(flags, anchor) {
} }
/** /**
* @param {any | import('../../types.js').Signal<any>} item * @param {any | import('../../types.js').Value<any>} item
* @param {number | import('../../types.js').Signal<number>} index * @param {number | import('../../types.js').Value<number>} index
* @param {null | unknown} key * @param {null | unknown} key
* @returns {import('../../types.js').EachItemBlock} * @returns {import('../../types.js').EachItemBlock}
*/ */
@ -97,7 +91,7 @@ export function create_each_item_block(item, index, key) {
* @param {() => V[]} collection * @param {() => V[]} collection
* @param {number} flags * @param {number} flags
* @param {null | ((item: V) => string)} key_fn * @param {null | ((item: V) => string)} key_fn
* @param {(anchor: null, item: V, index: import('../../types.js').MaybeSignal<number>) => void} render_fn * @param {(anchor: null, item: V, index: import('#client').MaybeSource<number>) => void} render_fn
* @param {null | ((anchor: Node) => void)} fallback_fn * @param {null | ((anchor: Node) => void)} fallback_fn
* @param {typeof reconcile_indexed_array | reconcile_tracked_array} reconcile_fn * @param {typeof reconcile_indexed_array | reconcile_tracked_array} reconcile_fn
* @returns {void} * @returns {void}
@ -116,7 +110,7 @@ function each(anchor_node, collection, flags, key_fn, render_fn, fallback_fn, re
/** @type {Array<string> | null} */ /** @type {Array<string> | null} */
let keys = null; let keys = null;
/** @type {null | import('../../types.js').EffectSignal} */ /** @type {null | import('../../types.js').Effect} */
let render = null; let render = null;
/** /**
@ -139,7 +133,7 @@ function each(anchor_node, collection, flags, key_fn, render_fn, fallback_fn, re
remove(fallback.d); remove(fallback.d);
fallback.d = null; fallback.d = null;
} }
destroy_signal(fallback.e); destroy_effect(fallback.e);
fallback.e = null; fallback.e = null;
} }
} }
@ -267,7 +261,7 @@ function each(anchor_node, collection, flags, key_fn, render_fn, fallback_fn, re
set_current_hydration_fragment([]); set_current_hydration_fragment([]);
} }
push_destroy_fn(each, () => { each.ondestroy = () => {
const flags = block.f; const flags = block.f;
const anchor_node = block.a; const anchor_node = block.a;
const is_controlled = (flags & EACH_IS_CONTROLLED) !== 0; const is_controlled = (flags & EACH_IS_CONTROLLED) !== 0;
@ -279,14 +273,14 @@ function each(anchor_node, collection, flags, key_fn, render_fn, fallback_fn, re
} }
const effect = fallback.e; const effect = fallback.e;
if (effect !== null) { if (effect !== null) {
destroy_signal(effect); destroy_effect(effect);
} }
fallback = fallback.p; fallback = fallback.p;
} }
// Clear the array // Clear the array
reconcile_fn([], block, anchor_node, is_controlled, render_fn, flags, false, keys); reconcile_fn([], block, anchor_node, is_controlled, render_fn, flags, false, keys);
destroy_signal(/** @type {import('../../types.js').EffectSignal} */ (render)); destroy_effect(/** @type {import('#client').Effect} */ (render));
}); };
block.e = each; block.e = each;
} }
@ -297,7 +291,7 @@ function each(anchor_node, collection, flags, key_fn, render_fn, fallback_fn, re
* @param {() => V[]} collection * @param {() => V[]} collection
* @param {number} flags * @param {number} flags
* @param {null | ((item: V) => string)} key_fn * @param {null | ((item: V) => string)} key_fn
* @param {(anchor: null, item: V, index: import('../../types.js').MaybeSignal<number>) => void} render_fn * @param {(anchor: null, item: V, index: import('#client').MaybeSource<number>) => void} render_fn
* @param {null | ((anchor: Node) => void)} fallback_fn * @param {null | ((anchor: Node) => void)} fallback_fn
* @returns {void} * @returns {void}
*/ */
@ -310,7 +304,7 @@ export function each_keyed(anchor_node, collection, flags, key_fn, render_fn, fa
* @param {Element | Comment} anchor_node * @param {Element | Comment} anchor_node
* @param {() => V[]} collection * @param {() => V[]} collection
* @param {number} flags * @param {number} flags
* @param {(anchor: null, item: V, index: import('../../types.js').MaybeSignal<number>) => void} render_fn * @param {(anchor: null, item: V, index: import('#client').MaybeSource<number>) => void} render_fn
* @param {null | ((anchor: Node) => void)} fallback_fn * @param {null | ((anchor: Node) => void)} fallback_fn
* @returns {void} * @returns {void}
*/ */
@ -324,7 +318,7 @@ export function each_indexed(anchor_node, collection, flags, render_fn, fallback
* @param {import('../../types.js').EachBlock} each_block * @param {import('../../types.js').EachBlock} each_block
* @param {Element | Comment | Text} dom * @param {Element | Comment | Text} dom
* @param {boolean} is_controlled * @param {boolean} is_controlled
* @param {(anchor: null, item: V, index: number | import('../../types.js').Signal<number>) => void} render_fn * @param {(anchor: null, item: V, index: number | import('../../types.js').Source<number>) => void} render_fn
* @param {number} flags * @param {number} flags
* @param {boolean} apply_transitions * @param {boolean} apply_transitions
* @returns {void} * @returns {void}
@ -338,6 +332,11 @@ function reconcile_indexed_array(
flags, flags,
apply_transitions apply_transitions
) { ) {
// If we are working with an array that isn't proxied or frozen, then remove strict equality and ensure the items
// are treated as reactive, so they get wrapped in a signal.
if ((flags & EACH_IS_STRICT_EQUALS) !== 0 && !is_frozen(array) && !(STATE_SYMBOL in array)) {
flags ^= EACH_IS_STRICT_EQUALS;
}
var a_blocks = each_block.v; var a_blocks = each_block.v;
var active_transitions = each_block.s; var active_transitions = each_block.s;
@ -393,7 +392,8 @@ function reconcile_indexed_array(
b_blocks[index] = block; b_blocks[index] = block;
hydrating_node = /** @type {import('../../types.js').TemplateNode} */ ( hydrating_node = /** @type {import('../../types.js').TemplateNode} */ (
/** @type {Node} */ (/** @type {Node} */ (fragment.at(-1)).nextSibling).nextSibling /** @type {Node} */ (/** @type {Node} */ (fragment[fragment.length - 1]).nextSibling)
.nextSibling
); );
} }
@ -438,7 +438,7 @@ function reconcile_indexed_array(
* @param {import('../../types.js').EachBlock} each_block * @param {import('../../types.js').EachBlock} each_block
* @param {Element | Comment | Text} dom * @param {Element | Comment | Text} dom
* @param {boolean} is_controlled * @param {boolean} is_controlled
* @param {(anchor: null, item: V, index: number | import('../../types.js').Signal<number>) => void} render_fn * @param {(anchor: null, item: V, index: number | import('../../types.js').Source<number>) => void} render_fn
* @param {number} flags * @param {number} flags
* @param {boolean} apply_transitions * @param {boolean} apply_transitions
* @param {Array<string> | null} keys * @param {Array<string> | null} keys
@ -454,6 +454,15 @@ function reconcile_tracked_array(
apply_transitions, apply_transitions,
keys keys
) { ) {
// If we are working with an array that isn't proxied or frozen, then remove strict equality and ensure the items
// are treated as reactive, so they get wrapped in a signal.
if ((flags & EACH_IS_STRICT_EQUALS) !== 0 && !is_frozen(array) && !(STATE_SYMBOL in array)) {
flags ^= EACH_IS_STRICT_EQUALS;
// Additionally as we're in an keyed each block, we'll need ensure the itens are all wrapped in signals.
if ((flags & EACH_ITEM_REACTIVE) === 0) {
flags ^= EACH_ITEM_REACTIVE;
}
}
var a_blocks = each_block.v; var a_blocks = each_block.v;
const is_computed_key = keys !== null; const is_computed_key = keys !== null;
var active_transitions = each_block.s; var active_transitions = each_block.s;
@ -517,7 +526,8 @@ function reconcile_tracked_array(
// Get the <!--ssr:..--> tag of the next item in the list // Get the <!--ssr:..--> tag of the next item in the list
// The fragment array can be empty if each block has no content // The fragment array can be empty if each block has no content
hydrating_node = /** @type {import('../../types.js').TemplateNode} */ ( hydrating_node = /** @type {import('../../types.js').TemplateNode} */ (
/** @type {Node} */ ((fragment.at(-1) || hydrating_node).nextSibling).nextSibling /** @type {Node} */ ((fragment[fragment.length - 1] || hydrating_node).nextSibling)
.nextSibling
); );
} }
@ -842,7 +852,7 @@ export function get_first_element(block) {
function update_each_item_block(block, item, index, type) { function update_each_item_block(block, item, index, type) {
const block_v = block.v; const block_v = block.v;
if ((type & EACH_ITEM_REACTIVE) !== 0) { if ((type & EACH_ITEM_REACTIVE) !== 0) {
set_signal_value(block_v, item); set(block_v, item);
} }
const transitions = block.s; const transitions = block.s;
const index_is_reactive = (type & EACH_INDEX_REACTIVE) !== 0; const index_is_reactive = (type & EACH_INDEX_REACTIVE) !== 0;
@ -852,7 +862,7 @@ function update_each_item_block(block, item, index, type) {
each_animation(block, transitions); each_animation(block, transitions);
} }
if (index_is_reactive) { if (index_is_reactive) {
set_signal_value(/** @type {import('../../types.js').Signal<number>} */ (block.i), index); set(/** @type {import('../../types.js').Value<number>} */ (block.i), index);
} else { } else {
block.i = index; block.i = index;
} }
@ -894,7 +904,7 @@ export function destroy_each_item_block(
if (!controlled && dom !== null) { if (!controlled && dom !== null) {
remove(dom); remove(dom);
} }
destroy_signal(/** @type {import('../../types.js').EffectSignal} */ (block.e)); destroy_effect(/** @type {import('#client').Effect} */ (block.e));
} }
/** /**
@ -902,7 +912,7 @@ export function destroy_each_item_block(
* @param {V} item * @param {V} item
* @param {unknown} key * @param {unknown} key
* @param {number} index * @param {number} index
* @param {(anchor: null, item: V, index: number | import('../../types.js').Signal<number>) => void} render_fn * @param {(anchor: null, item: V, index: number | import('../../types.js').Value<number>) => void} render_fn
* @param {number} flags * @param {number} flags
* @returns {import('../../types.js').EachItemBlock} * @returns {import('../../types.js').EachItemBlock}
*/ */

@ -0,0 +1,29 @@
import { render_effect } from '../../reactivity/effects.js';
import { reconcile_html, remove } from '../reconciler.js';
/**
* @param {Element | Text | Comment} dom
* @param {() => string} get_value
* @param {boolean} svg
* @returns {void}
*/
export function html(dom, get_value, svg) {
/** @type {import('#client').TemplateNode | import('#client').TemplateNode[]} */
let html_dom;
/** @type {string} */
let value;
const effect = render_effect(() => {
if (value !== (value = get_value())) {
if (html_dom) remove(html_dom);
html_dom = reconcile_html(dom, value, svg);
}
});
effect.ondestroy = () => {
if (html_dom) {
remove(html_dom);
}
};
}

@ -4,13 +4,13 @@ import {
hydrate_block_anchor, hydrate_block_anchor,
hydrating, hydrating,
set_current_hydration_fragment set_current_hydration_fragment
} from '../../hydration.js'; } from '../hydration.js';
import { remove } from '../../reconciler.js'; import { remove } from '../reconciler.js';
import { current_block, destroy_signal, execute_effect, push_destroy_fn } from '../../runtime.js'; import { current_block, execute_effect } from '../../runtime.js';
import { render_effect } from '../../reactivity/computations.js'; import { destroy_effect, render_effect } from '../../reactivity/effects.js';
import { trigger_transitions } from '../../transitions.js'; import { trigger_transitions } from '../elements/transitions.js';
/** @returns {import('../../types.js').IfBlock} */ /** @returns {import('#client').IfBlock} */
function create_if_block() { function create_if_block() {
return { return {
// alternate transitions // alternate transitions
@ -26,7 +26,7 @@ function create_if_block() {
// effect // effect
e: null, e: null,
// parent // parent
p: /** @type {import('../../types.js').Block} */ (current_block), p: /** @type {import('#client').Block} */ (current_block),
// transition // transition
r: null, r: null,
// type // type
@ -45,137 +45,157 @@ function create_if_block() {
*/ */
export function if_block(anchor_node, condition_fn, consequent_fn, alternate_fn) { export function if_block(anchor_node, condition_fn, consequent_fn, alternate_fn) {
const block = create_if_block(); const block = create_if_block();
hydrate_block_anchor(anchor_node); hydrate_block_anchor(anchor_node);
/** Whether or not there was a hydration mismatch. Needs to be a `let` or else it isn't treeshaken out */ /** Whether or not there was a hydration mismatch. Needs to be a `let` or else it isn't treeshaken out */
let mismatch = false; let mismatch = false;
/** @type {null | import('../../types.js').TemplateNode | Array<import('../../types.js').TemplateNode>} */ /** @type {null | import('#client').TemplateNode | Array<import('#client').TemplateNode>} */
let consequent_dom = null; let consequent_dom = null;
/** @type {null | import('../../types.js').TemplateNode | Array<import('../../types.js').TemplateNode>} */
/** @type {null | import('#client').TemplateNode | Array<import('#client').TemplateNode>} */
let alternate_dom = null; let alternate_dom = null;
let has_mounted = false; let has_mounted = false;
/** /**
* @type {import('../../types.js').EffectSignal | null} * @type {import('#client').Effect | null}
*/ */
let current_branch_effect = null; let current_branch_effect = null;
const if_effect = render_effect( /** @type {import('#client').Effect} */
() => { let consequent_effect;
const result = !!condition_fn();
if (block.v !== result || !has_mounted) { /** @type {import('#client').Effect} */
block.v = result; let alternate_effect;
if (has_mounted) {
const consequent_transitions = block.c; const if_effect = render_effect(() => {
const alternate_transitions = block.a; const result = !!condition_fn();
if (result) {
if (alternate_transitions === null || alternate_transitions.size === 0) { if (block.v !== result || !has_mounted) {
execute_effect(alternate_effect); block.v = result;
} else {
trigger_transitions(alternate_transitions, 'out'); if (has_mounted) {
} const consequent_transitions = block.c;
if (consequent_transitions === null || consequent_transitions.size === 0) { const alternate_transitions = block.a;
execute_effect(consequent_effect);
} else { if (result) {
trigger_transitions(consequent_transitions, 'in'); if (alternate_transitions === null || alternate_transitions.size === 0) {
} execute_effect(alternate_effect);
} else { } else {
if (consequent_transitions === null || consequent_transitions.size === 0) { trigger_transitions(alternate_transitions, 'out');
execute_effect(consequent_effect);
} else {
trigger_transitions(consequent_transitions, 'out');
}
if (alternate_transitions === null || alternate_transitions.size === 0) {
execute_effect(alternate_effect);
} else {
trigger_transitions(alternate_transitions, 'in');
}
} }
} else if (hydrating) {
const comment_text = /** @type {Comment} */ (current_hydration_fragment?.[0])?.data; if (consequent_transitions === null || consequent_transitions.size === 0) {
if ( execute_effect(consequent_effect);
!comment_text ||
(comment_text === 'ssr:if:true' && !result) ||
(comment_text === 'ssr:if:false' && result)
) {
// Hydration mismatch: remove everything inside the anchor and start fresh.
// This could happen using when `{#if browser} .. {/if}` in SvelteKit.
remove(current_hydration_fragment);
set_current_hydration_fragment(null);
mismatch = true;
} else { } else {
// Remove the ssr:if comment node or else it will confuse the subsequent hydration algorithm trigger_transitions(consequent_transitions, 'in');
current_hydration_fragment.shift(); }
} else {
if (consequent_transitions === null || consequent_transitions.size === 0) {
execute_effect(consequent_effect);
} else {
trigger_transitions(consequent_transitions, 'out');
}
if (alternate_transitions === null || alternate_transitions.size === 0) {
execute_effect(alternate_effect);
} else {
trigger_transitions(alternate_transitions, 'in');
} }
} }
has_mounted = true; } else if (hydrating) {
} const comment_text = /** @type {Comment} */ (current_hydration_fragment?.[0])?.data;
},
block, if (
false !comment_text ||
); (comment_text === 'ssr:if:true' && !result) ||
// Managed effect (comment_text === 'ssr:if:false' && result)
const consequent_effect = render_effect( ) {
( // Hydration mismatch: remove everything inside the anchor and start fresh.
/** @type {any} */ _, // This could happen using when `{#if browser} .. {/if}` in SvelteKit.
/** @type {import('../../types.js').EffectSignal | null} */ consequent_effect remove(current_hydration_fragment);
) => { set_current_hydration_fragment(null);
const result = block.v; mismatch = true;
if (!result && consequent_dom !== null) { } else {
remove(consequent_dom); // Remove the ssr:if comment node or else it will confuse the subsequent hydration algorithm
consequent_dom = null; current_hydration_fragment.shift();
}
if (result && current_branch_effect !== consequent_effect) {
consequent_fn(anchor_node);
if (mismatch && current_branch_effect === null) {
// Set fragment so that Svelte continues to operate in hydration mode
set_current_hydration_fragment([]);
} }
current_branch_effect = consequent_effect;
consequent_dom = block.d;
} }
block.d = null;
}, has_mounted = true;
block, }
true
); // create these here so they have the correct parent/child relationship
block.ce = consequent_effect; consequent_effect ??= render_effect(
// Managed effect (/** @type {any} */ _, /** @type {import('#client').Effect | null} */ consequent_effect) => {
const alternate_effect = render_effect( const result = block.v;
(
/** @type {any} */ _, if (!result && consequent_dom !== null) {
/** @type {import('../../types.js').EffectSignal | null} */ alternate_effect remove(consequent_dom);
) => { consequent_dom = null;
const result = block.v;
if (result && alternate_dom !== null) {
remove(alternate_dom);
alternate_dom = null;
}
if (!result && current_branch_effect !== alternate_effect) {
if (alternate_fn !== null) {
alternate_fn(anchor_node);
} }
if (mismatch && current_branch_effect === null) {
// Set fragment so that Svelte continues to operate in hydration mode if (result && current_branch_effect !== consequent_effect) {
set_current_hydration_fragment([]); consequent_fn(anchor_node);
if (mismatch && current_branch_effect === null) {
// Set fragment so that Svelte continues to operate in hydration mode
set_current_hydration_fragment([]);
}
current_branch_effect = consequent_effect;
consequent_dom = block.d;
} }
current_branch_effect = alternate_effect;
alternate_dom = block.d; block.d = null;
} },
block.d = null; block,
}, true
block, );
true block.ce = consequent_effect;
);
block.ae = alternate_effect; alternate_effect ??= render_effect(
push_destroy_fn(if_effect, () => { (/** @type {any} */ _, /** @type {import('#client').Effect | null} */ alternate_effect) => {
const result = block.v;
if (result && alternate_dom !== null) {
remove(alternate_dom);
alternate_dom = null;
}
if (!result && current_branch_effect !== alternate_effect) {
if (alternate_fn !== null) {
alternate_fn(anchor_node);
}
if (mismatch && current_branch_effect === null) {
// Set fragment so that Svelte continues to operate in hydration mode
set_current_hydration_fragment([]);
}
current_branch_effect = alternate_effect;
alternate_dom = block.d;
}
block.d = null;
},
block,
true
);
block.ae = alternate_effect;
}, block);
if_effect.ondestroy = () => {
if (consequent_dom !== null) { if (consequent_dom !== null) {
remove(consequent_dom); remove(consequent_dom);
} }
if (alternate_dom !== null) { if (alternate_dom !== null) {
remove(alternate_dom); remove(alternate_dom);
} }
destroy_signal(consequent_effect);
destroy_signal(alternate_effect); destroy_effect(consequent_effect);
}); destroy_effect(alternate_effect);
};
block.e = if_effect; block.e = if_effect;
} }

@ -1,9 +1,9 @@
import { UNINITIALIZED, KEY_BLOCK } from '../../constants.js'; import { UNINITIALIZED, KEY_BLOCK } from '../../constants.js';
import { hydrate_block_anchor } from '../../hydration.js'; import { hydrate_block_anchor } from '../hydration.js';
import { remove } from '../../reconciler.js'; import { remove } from '../reconciler.js';
import { current_block, destroy_signal, execute_effect, push_destroy_fn } from '../../runtime.js'; import { current_block, execute_effect } from '../../runtime.js';
import { render_effect } from '../../reactivity/computations.js'; import { destroy_effect, render_effect } from '../../reactivity/effects.js';
import { trigger_transitions } from '../../transitions.js'; import { trigger_transitions } from '../elements/transitions.js';
import { safe_not_equal } from '../../reactivity/equality.js'; import { safe_not_equal } from '../../reactivity/equality.js';
/** @returns {import('../../types.js').KeyBlock} */ /** @returns {import('../../types.js').KeyBlock} */
@ -58,7 +58,7 @@ export function key_block(anchor_node, key, render_fn) {
remove(render.d); remove(render.d);
render.d = null; render.d = null;
} }
destroy_signal(render.e); destroy_effect(render.e);
render.e = null; render.e = null;
} }
} }
@ -122,7 +122,7 @@ export function key_block(anchor_node, key, render_fn) {
// we trigger the effect after. // we trigger the effect after.
render(); render();
mounted = true; mounted = true;
push_destroy_fn(key_effect, () => { key_effect.ondestroy = () => {
let render = current_render; let render = current_render;
while (render !== null) { while (render !== null) {
const dom = render.d; const dom = render.d;
@ -131,10 +131,10 @@ export function key_block(anchor_node, key, render_fn) {
} }
const effect = render.e; const effect = render.e;
if (effect !== null) { if (effect !== null) {
destroy_signal(effect); destroy_effect(effect);
} }
render = render.p; render = render.p;
} }
}); };
block.e = key_effect; block.e = key_effect;
} }

@ -0,0 +1,40 @@
import { SNIPPET_BLOCK } from '../../constants.js';
import { render_effect } from '../../reactivity/effects.js';
import { remove } from '../reconciler.js';
import { current_block, untrack } from '../../runtime.js';
/**
* @param {() => Function | null | undefined} get_snippet
* @param {Node} node
* @param {(() => any)[]} args
* @returns {void}
*/
export function snippet(get_snippet, node, ...args) {
/** @type {import('#client').SnippetBlock} */
const block = {
// dom
d: null,
// parent
p: /** @type {import('#client').Block} */ (current_block),
// effect
e: null,
// transition
r: null,
// type
t: SNIPPET_BLOCK
};
render_effect(() => {
// Only rerender when the snippet function itself changes,
// not when an eagerly-read prop inside the snippet function changes
const snippet = get_snippet();
if (snippet) {
untrack(() => snippet(node, ...args));
}
return () => {
if (block.d !== null) {
remove(block.d);
}
};
}, block);
}

@ -0,0 +1,147 @@
import { DYNAMIC_COMPONENT_BLOCK } from '../../constants.js';
import { hydrate_block_anchor } from '../hydration.js';
import { destroy_effect, render_effect } from '../../reactivity/effects.js';
import { remove } from '../reconciler.js';
import { current_block, execute_effect } from '../../runtime.js';
import { trigger_transitions } from '../elements/transitions.js';
/**
* @template P
* @param {Comment} anchor_node
* @param {() => (props: P) => void} component_fn
* @param {(component: (props: P) => void) => void} render_fn
* @returns {void}
*/
export function component(anchor_node, component_fn, render_fn) {
/** @type {import('#client').DynamicComponentBlock} */
const block = {
// dom
d: null,
// effect
e: null,
// parent
p: /** @type {import('#client').Block} */ (current_block),
// transition
r: null,
// type
t: DYNAMIC_COMPONENT_BLOCK
};
/** @type {null | import('#client').Render} */
let current_render = null;
hydrate_block_anchor(anchor_node);
/** @type {null | ((props: P) => void)} */
let component = null;
block.r =
/**
* @param {import('#client').Transition} transition
* @returns {void}
*/
(transition) => {
const render = /** @type {import('#client').Render} */ (current_render);
const transitions = render.s;
transitions.add(transition);
transition.f(() => {
transitions.delete(transition);
if (transitions.size === 0) {
// If the current render has changed since, then we can remove the old render
// effect as it's stale.
if (current_render !== render && render.e !== null) {
if (render.d !== null) {
remove(render.d);
render.d = null;
}
destroy_effect(render.e);
render.e = null;
}
}
});
};
const create_render_effect = () => {
/** @type {import('#client').Render} */
const render = {
d: null,
e: null,
s: new Set(),
p: current_render
};
// Managed effect
render.e = render_effect(
() => {
const current = block.d;
if (current !== null) {
remove(current);
block.d = null;
}
if (component) {
render_fn(component);
}
render.d = block.d;
block.d = null;
},
block,
true
);
current_render = render;
};
const render = () => {
const render = current_render;
if (render === null) {
create_render_effect();
return;
}
const transitions = render.s;
if (transitions.size === 0) {
if (render.d !== null) {
remove(render.d);
render.d = null;
}
if (render.e) {
execute_effect(render.e);
} else {
create_render_effect();
}
} else {
create_render_effect();
trigger_transitions(transitions, 'out');
}
};
const component_effect = render_effect(
() => {
const next_component = component_fn();
if (component !== next_component) {
component = next_component;
render();
}
},
block,
false
);
component_effect.ondestroy = () => {
let render = current_render;
while (render !== null) {
const dom = render.d;
if (dom !== null) {
remove(dom);
}
const effect = render.e;
if (effect !== null) {
destroy_effect(effect);
}
render = render.p;
}
};
block.e = component_effect;
}

@ -0,0 +1,139 @@
import { namespace_svg } from '../../../../constants.js';
import { DYNAMIC_ELEMENT_BLOCK } from '../../constants.js';
import { current_hydration_fragment, hydrate_block_anchor, hydrating } from '../hydration.js';
import { empty } from '../operations.js';
import { destroy_effect, render_effect } from '../../reactivity/effects.js';
import { insert, remove } from '../reconciler.js';
import { current_block, execute_effect } from '../../runtime.js';
import { is_array } from '../../utils.js';
/**
* @param {import('#client').Block} block
* @param {Element} from
* @param {Element} to
* @returns {void}
*/
function swap_block_dom(block, from, to) {
const dom = block.d;
if (is_array(dom)) {
for (let i = 0; i < dom.length; i++) {
if (dom[i] === from) {
dom[i] = to;
break;
}
}
} else if (dom === from) {
block.d = to;
}
}
/**
* @param {Comment} anchor_node
* @param {() => string} tag_fn
* @param {boolean | null} is_svg `null` == not statically known
* @param {undefined | ((element: Element, anchor: Node) => void)} render_fn
* @returns {void}
*/
export function element(anchor_node, tag_fn, is_svg, render_fn) {
/** @type {import('#client').DynamicElementBlock} */
const block = {
// dom
d: null,
// effect
e: null,
// parent
p: /** @type {import('#client').Block} */ (current_block),
// transition
r: null,
// type
t: DYNAMIC_ELEMENT_BLOCK
};
hydrate_block_anchor(anchor_node);
let has_mounted = false;
/** @type {string} */
let tag;
/** @type {null | Element} */
let element = null;
const element_effect = render_effect(
() => {
tag = tag_fn();
if (has_mounted) {
execute_effect(render_effect_signal);
}
has_mounted = true;
},
block,
false
);
// Managed effect
const render_effect_signal = render_effect(
() => {
// We try our best infering the namespace in case it's not possible to determine statically,
// but on the first render on the client (without hydration) the parent will be undefined,
// since the anchor is not attached to its parent / the dom yet.
const ns =
is_svg || tag === 'svg'
? namespace_svg
: is_svg === false || anchor_node.parentElement?.tagName === 'foreignObject'
? null
: anchor_node.parentElement?.namespaceURI ?? null;
const next_element = tag
? hydrating
? /** @type {Element} */ (current_hydration_fragment[0])
: ns
? document.createElementNS(ns, tag)
: document.createElement(tag)
: null;
const prev_element = element;
if (prev_element !== null) {
block.d = null;
}
element = next_element;
if (element !== null && render_fn !== undefined) {
let anchor;
if (hydrating) {
// Use the existing ssr comment as the anchor so that the inner open and close
// methods can pick up the existing nodes correctly
anchor = /** @type {Comment} */ (element.firstChild);
} else {
anchor = empty();
element.appendChild(anchor);
}
render_fn(element, anchor);
}
const has_prev_element = prev_element !== null;
if (has_prev_element) {
remove(prev_element);
}
if (element !== null) {
insert(element, null, anchor_node);
if (has_prev_element) {
const parent_block = block.p;
swap_block_dom(parent_block, prev_element, element);
}
}
},
block,
true
);
element_effect.ondestroy = () => {
if (element !== null) {
remove(element);
block.d = null;
element = null;
}
destroy_effect(render_effect_signal);
};
block.e = element_effect;
}

@ -0,0 +1,76 @@
import { HEAD_BLOCK } from '../../constants.js';
import {
current_hydration_fragment,
get_hydration_fragment,
hydrating,
set_current_hydration_fragment
} from '../hydration.js';
import { empty } from '../operations.js';
import { render_effect } from '../../reactivity/effects.js';
import { remove } from '../reconciler.js';
import { current_block } from '../../runtime.js';
/**
* @param {(anchor: Node | null) => void} render_fn
* @returns {void}
*/
export function head(render_fn) {
/** @type {import('#client').HeadBlock} */
const block = {
// dom
d: null,
// effect
e: null,
// parent
p: /** @type {import('#client').Block} */ (current_block),
// transition
r: null,
// type
t: HEAD_BLOCK
};
// The head function may be called after the first hydration pass and ssr comment nodes may still be present,
// therefore we need to skip that when we detect that we're not in hydration mode.
let hydration_fragment = null;
let previous_hydration_fragment = null;
let is_hydrating = hydrating;
if (is_hydrating) {
hydration_fragment = get_hydration_fragment(document.head.firstChild);
previous_hydration_fragment = current_hydration_fragment;
set_current_hydration_fragment(hydration_fragment);
}
try {
const head_effect = render_effect(
() => {
const current = block.d;
if (current !== null) {
remove(current);
block.d = null;
}
let anchor = null;
if (!hydrating) {
anchor = empty();
document.head.appendChild(anchor);
}
render_fn(anchor);
},
block,
false
);
head_effect.ondestroy = () => {
const current = block.d;
if (current !== null) {
remove(current);
}
};
block.e = head_effect;
} finally {
if (is_hydrating) {
set_current_hydration_fragment(previous_hydration_fragment);
}
}
}

@ -0,0 +1,53 @@
import { effect } from '../../reactivity/effects.js';
import { deep_read_state, untrack } from '../../runtime.js';
/**
* @template P
* @param {Element} dom
* @param {(dom: Element, value?: P) => import('#client').ActionPayload<P>} action
* @param {() => P} [value_fn]
* @returns {void}
*/
export function action(dom, action, value_fn) {
/** @type {undefined | import('#client').ActionPayload<P>} */
var payload = undefined;
var needs_deep_read = false;
// Action could come from a prop, therefore could be a signal, therefore untrack
// TODO we could take advantage of this and enable https://github.com/sveltejs/svelte/issues/6942
effect(() => {
if (value_fn) {
var value = value_fn();
untrack(() => {
if (payload === undefined) {
payload = action(dom, value) || {};
needs_deep_read = !!payload?.update;
} else {
var update = payload.update;
if (typeof update === 'function') {
update(value);
}
}
});
// Action's update method is coarse-grained, i.e. when anything in the passed value changes, update.
// This works in legacy mode because of mutable_source being updated as a whole, but when using $state
// together with actions and mutation, it wouldn't notice the change without a deep read.
if (needs_deep_read) {
deep_read_state(value);
}
} else {
untrack(() => (payload = action(dom)));
}
});
effect(() => {
if (payload !== undefined) {
var destroy = payload.destroy;
if (typeof destroy === 'function') {
return () => {
/** @type {Function} */ (destroy)();
};
}
}
});
}

@ -0,0 +1,362 @@
import { DEV } from 'esm-env';
import { hydrating } from '../hydration.js';
import { render_effect } from '../../reactivity/effects.js';
import { get_descriptors, map_get, map_set, object_assign } from '../../utils.js';
import { AttributeAliases, DelegatedEvents, namespace_svg } from '../../../../constants.js';
import { delegate } from './events.js';
import { autofocus } from './misc.js';
/**
* 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.
* @param {HTMLInputElement | HTMLSelectElement} dom
* @returns {void}
*/
export function remove_input_attr_defaults(dom) {
if (hydrating) {
attr(dom, 'value', null);
attr(dom, 'checked', null);
}
}
/**
* @param {Element} dom
* @param {string} attribute
* @param {() => string} value
*/
export function attr_effect(dom, attribute, value) {
render_effect(() => {
attr(dom, attribute, value());
});
}
/**
* @param {Element} dom
* @param {string} attribute
* @param {string | null} value
*/
export function attr(dom, attribute, value) {
value = value == null ? null : value + '';
if (DEV) {
check_src_in_dev_hydration(dom, attribute, value);
}
if (
!hydrating ||
(dom.getAttribute(attribute) !== value &&
// If we reset those, they would result in another network request, which we want to avoid.
// We assume they are the same between client and server as checking if they are equal is expensive
// (we can't just compare the strings as they can be different between client and server but result in the
// same url, so we would need to create hidden anchor elements to compare them)
attribute !== 'src' &&
attribute !== 'href' &&
attribute !== 'srcset')
) {
if (value === null) {
dom.removeAttribute(attribute);
} else {
dom.setAttribute(attribute, value);
}
}
}
/**
* @param {Element} dom
* @param {string} attribute
* @param {() => string} value
*/
export function xlink_attr_effect(dom, attribute, value) {
render_effect(() => {
xlink_attr(dom, attribute, value());
});
}
/**
* @param {Element} dom
* @param {string} attribute
* @param {string} value
*/
export function xlink_attr(dom, attribute, value) {
dom.setAttributeNS('http://www.w3.org/1999/xlink', attribute, value);
}
/**
* @param {any} node
* @param {string} prop
* @param {() => any} value
*/
export function set_custom_element_data_effect(node, prop, value) {
render_effect(() => {
set_custom_element_data(node, prop, value());
});
}
/**
* @param {any} node
* @param {string} prop
* @param {any} value
*/
export function set_custom_element_data(node, prop, value) {
if (prop in node) {
node[prop] = typeof node[prop] === 'boolean' && value === '' ? true : value;
} else {
attr(node, prop, value);
}
}
/**
* Like `spread_attributes` but self-contained
* @param {Element & ElementCSSInlineStyle} dom
* @param {() => Record<string, unknown>[]} attrs
* @param {boolean} lowercase_attributes
* @param {string} css_hash
*/
export function spread_attributes_effect(dom, attrs, lowercase_attributes, css_hash) {
/** @type {Record<string, any> | undefined} */
var current;
render_effect(() => {
current = spread_attributes(dom, current, attrs(), lowercase_attributes, css_hash);
});
}
/**
* Spreads attributes onto a DOM element, taking into account the currently set attributes
* @param {Element & ElementCSSInlineStyle} dom
* @param {Record<string, unknown> | undefined} prev
* @param {Record<string, unknown>[]} attrs
* @param {boolean} lowercase_attributes
* @param {string} css_hash
* @returns {Record<string, unknown>}
*/
export function spread_attributes(dom, prev, attrs, lowercase_attributes, css_hash) {
var next = object_assign({}, ...attrs);
var has_hash = css_hash.length !== 0;
for (var key in prev) {
if (!(key in next)) {
next[key] = null;
}
}
if (has_hash && !next.class) {
next.class = '';
}
var setters = map_get(setters_cache, dom.nodeName);
if (!setters) map_set(setters_cache, dom.nodeName, (setters = get_setters(dom)));
for (key in next) {
var value = next[key];
if (value === prev?.[key]) continue;
var prefix = key[0] + key[1]; // this is faster than key.slice(0, 2)
if (prefix === '$$') continue;
if (prefix === 'on') {
/** @type {{ capture?: true }} */
var opts = {};
var event_name = key.slice(2);
var delegated = DelegatedEvents.includes(event_name);
if (
event_name.endsWith('capture') &&
event_name !== 'ongotpointercapture' &&
event_name !== 'onlostpointercapture'
) {
event_name = event_name.slice(0, -7);
opts.capture = true;
}
if (!delegated && prev?.[key]) {
dom.removeEventListener(event_name, /** @type {any} */ (prev[key]), opts);
}
if (value != null) {
if (!delegated) {
dom.addEventListener(event_name, value, opts);
} else {
// @ts-ignore
dom[`__${event_name}`] = value;
delegate([event_name]);
}
}
} else if (value == null) {
dom.removeAttribute(key);
} else if (key === 'style') {
dom.style.cssText = value + '';
} else if (key === 'autofocus') {
autofocus(/** @type {HTMLElement} */ (dom), Boolean(value));
} else if (key === '__value' || key === 'value') {
// @ts-ignore
dom.value = dom[key] = dom.__value = value;
} else {
var name = key;
if (lowercase_attributes) {
name = name.toLowerCase();
name = AttributeAliases[name] || name;
}
if (setters.includes(name)) {
if (DEV) {
check_src_in_dev_hydration(dom, name, value);
}
if (
!hydrating ||
// @ts-ignore see attr method for an explanation of src/srcset
(dom[name] !== value && name !== 'src' && name !== 'href' && name !== 'srcset')
) {
// @ts-ignore
dom[name] = value;
}
} else if (typeof value !== 'function') {
if (has_hash && name === 'class') {
if (value) value += ' ';
value += css_hash;
}
attr(dom, name, value);
}
}
}
return next;
}
/**
* @param {Element} node
* @param {() => Record<string, unknown>[]} attrs
* @param {string} css_hash
*/
export function spread_dynamic_element_attributes_effect(node, attrs, css_hash) {
/** @type {Record<string, any> | undefined} */
var current;
render_effect(() => {
current = spread_dynamic_element_attributes(node, current, attrs(), css_hash);
});
}
/**
* @param {Element} node
* @param {Record<string, unknown> | undefined} prev
* @param {Record<string, unknown>[]} attrs
* @param {string} css_hash
*/
export function spread_dynamic_element_attributes(node, prev, attrs, css_hash) {
if (node.tagName.includes('-')) {
var next = object_assign({}, ...attrs);
for (var key in prev) {
if (!(key in next)) {
next[key] = null;
}
}
for (key in next) {
set_custom_element_data(node, key, next[key]);
}
return next;
} else {
return spread_attributes(
/** @type {Element & ElementCSSInlineStyle} */ (node),
prev,
attrs,
node.namespaceURI !== namespace_svg,
css_hash
);
}
}
/**
* List of attributes that should always be set through the attr method,
* because updating them through the property setter doesn't work reliably.
* In the example of `width`/`height`, the problem is that the setter only
* accepts numeric values, but the attribute can also be set to a string like `50%`.
* If this list becomes too big, rethink this approach.
*/
var always_set_through_set_attribute = ['width', 'height'];
/** @type {Map<string, string[]>} */
var setters_cache = new Map();
/** @param {Element} element */
function get_setters(element) {
/** @type {string[]} */
var setters = [];
// @ts-expect-error
var descriptors = get_descriptors(element.__proto__);
for (var key in descriptors) {
if (descriptors[key].set && !always_set_through_set_attribute.includes(key)) {
setters.push(key);
}
}
return setters;
}
/**
* @param {any} dom
* @param {string} attribute
* @param {string | null} value
*/
function check_src_in_dev_hydration(dom, attribute, value) {
if (!hydrating) return;
if (attribute !== 'src' && attribute !== 'href' && attribute !== 'srcset') return;
if (attribute === 'srcset' && srcset_url_equal(dom, value)) return;
if (src_url_equal(dom.getAttribute(attribute) ?? '', value ?? '')) return;
// eslint-disable-next-line no-console
console.error(
`Detected a ${attribute} attribute value change during hydration. This will not be repaired during hydration, ` +
`the ${attribute} value that came from the server will be used. Related element:`,
dom,
' Differing value:',
value
);
}
/**
* @param {string} element_src
* @param {string} url
* @returns {boolean}
*/
function src_url_equal(element_src, url) {
if (element_src === url) return true;
return new URL(element_src, document.baseURI).href === new URL(url, document.baseURI).href;
}
/** @param {string} srcset */
function split_srcset(srcset) {
return srcset.split(',').map((src) => src.trim().split(' ').filter(Boolean));
}
/**
* @param {HTMLSourceElement | HTMLImageElement} element
* @param {string | undefined | null} srcset
* @returns {boolean}
*/
export function srcset_url_equal(element, srcset) {
var element_urls = split_srcset(element.srcset);
var urls = split_srcset(srcset ?? '');
return (
urls.length === element_urls.length &&
urls.every(
([url, width], i) =>
width === element_urls[i][1] &&
// We need to test both ways because Vite will create an a full URL with
// `new URL(asset, import.meta.url).href` for the client when `base: './'`, and the
// relative URLs inside srcset are not automatically resolved to absolute URLs by
// browsers (in contrast to img.src). This means both SSR and DOM code could
// contain relative or absolute URLs.
(src_url_equal(element_urls[i][0], url) || src_url_equal(url, element_urls[i][0]))
)
);
}

@ -0,0 +1,170 @@
import { DEV } from 'esm-env';
import { render_effect } from '../../../reactivity/effects.js';
import { stringify } from '../../../render.js';
/**
* @param {HTMLInputElement} input
* @param {() => unknown} get_value
* @param {(value: unknown) => void} update
* @returns {void}
*/
export function bind_value(input, get_value, update) {
input.addEventListener('input', () => {
if (DEV && input.type === 'checkbox') {
throw new Error(
'Using bind:value together with a checkbox input is not allowed. Use bind:checked instead'
);
}
update(is_numberlike_input(input) ? to_number(input.value) : input.value);
});
render_effect(() => {
if (DEV && input.type === 'checkbox') {
throw new Error(
'Using bind:value together with a checkbox input is not allowed. Use bind:checked instead'
);
}
var value = get_value();
// @ts-ignore
input.__value = value;
if (is_numberlike_input(input) && value === to_number(input.value)) {
// handles 0 vs 00 case (see https://github.com/sveltejs/svelte/issues/9959)
return;
}
if (input.type === 'date' && !value && !input.value) {
// Handles the case where a temporarily invalid date is set (while typing, for example with a leading 0 for the day)
// and prevents this state from clearing the other parts of the date input (see https://github.com/sveltejs/svelte/issues/7897)
return;
}
input.value = stringify(value);
});
}
/**
* @param {Array<HTMLInputElement>} inputs
* @param {null | [number]} group_index
* @param {HTMLInputElement} input
* @param {() => unknown} get_value
* @param {(value: unknown) => void} update
* @returns {void}
*/
export function bind_group(inputs, group_index, input, get_value, update) {
var is_checkbox = input.getAttribute('type') === 'checkbox';
var binding_group = inputs;
if (group_index !== null) {
for (var index of group_index) {
var group = binding_group;
// @ts-ignore
binding_group = group[index];
if (binding_group === undefined) {
// @ts-ignore
binding_group = group[index] = [];
}
}
}
binding_group.push(input);
input.addEventListener('change', () => {
// @ts-ignore
var value = input.__value;
if (is_checkbox) {
value = get_binding_group_value(binding_group, value, input.checked);
}
update(value);
});
render_effect(() => {
var value = get_value();
if (is_checkbox) {
value = value || [];
// @ts-ignore
input.checked = value.includes(input.__value);
} else {
// @ts-ignore
input.checked = input.__value === value;
}
});
render_effect(() => {
return () => {
var index = binding_group.indexOf(input);
if (index !== -1) {
binding_group.splice(index, 1);
}
};
});
}
/**
* @param {HTMLInputElement} input
* @param {() => unknown} get_value
* @param {(value: unknown) => void} update
* @returns {void}
*/
export function bind_checked(input, get_value, update) {
input.addEventListener('change', () => {
var value = input.checked;
update(value);
});
// eslint-disable-next-line eqeqeq
if (get_value() == undefined) {
update(false);
}
render_effect(() => {
var value = get_value();
input.checked = Boolean(value);
});
}
/**
* @template V
* @param {Array<HTMLInputElement>} group
* @param {V} __value
* @param {boolean} checked
* @returns {V[]}
*/
function get_binding_group_value(group, __value, checked) {
var value = new Set();
for (var i = 0; i < group.length; i += 1) {
if (group[i].checked) {
// @ts-ignore
value.add(group[i].__value);
}
}
if (!checked) {
value.delete(__value);
}
return Array.from(value);
}
/**
* @param {HTMLInputElement} input
*/
function is_numberlike_input(input) {
var type = input.type;
return type === 'number' || type === 'range';
}
/**
* @param {string} value
*/
function to_number(value) {
return value === '' ? null : +value;
}

@ -0,0 +1,278 @@
import { hydrating } from '../../hydration.js';
import { destroy_effect, managed_effect, render_effect } from '../../../reactivity/effects.js';
import { listen } from './shared.js';
/** @param {TimeRanges} ranges */
function time_ranges_to_array(ranges) {
var array = [];
for (var i = 0; i < ranges.length; i += 1) {
array.push({ start: ranges.start(i), end: ranges.end(i) });
}
return array;
}
/**
* @param {HTMLVideoElement | HTMLAudioElement} media
* @param {() => number | undefined} get_value
* @param {(value: number) => void} update
* @returns {void}
*/
export function bind_current_time(media, get_value, update) {
/** @type {number} */
var raf_id;
var updating = false;
// Ideally, listening to timeupdate would be enough, but it fires too infrequently for the currentTime
// binding, which is why we use a raf loop, too. We additionally still listen to timeupdate because
// the user could be scrubbing through the video using the native controls when the media is paused.
var callback = () => {
cancelAnimationFrame(raf_id);
if (!media.paused) {
raf_id = requestAnimationFrame(callback);
}
updating = true;
update(media.currentTime);
};
raf_id = requestAnimationFrame(callback);
media.addEventListener('timeupdate', callback);
render_effect(() => {
var value = get_value();
// through isNaN we also allow number strings, which is more robust
if (!updating && !isNaN(/** @type {any} */ (value))) {
media.currentTime = /** @type {number} */ (value);
}
updating = false;
});
render_effect(() => () => cancelAnimationFrame(raf_id));
}
/**
* @param {HTMLVideoElement | HTMLAudioElement} media
* @param {(array: Array<{ start: number; end: number }>) => void} update
*/
export function bind_buffered(media, update) {
listen(media, ['loadedmetadata', 'progress'], () => update(time_ranges_to_array(media.buffered)));
}
/**
* @param {HTMLVideoElement | HTMLAudioElement} media
* @param {(array: Array<{ start: number; end: number }>) => void} update
*/
export function bind_seekable(media, update) {
listen(media, ['loadedmetadata'], () => update(time_ranges_to_array(media.seekable)));
}
/**
* @param {HTMLVideoElement | HTMLAudioElement} media
* @param {(array: Array<{ start: number; end: number }>) => void} update
*/
export function bind_played(media, update) {
listen(media, ['timeupdate'], () => update(time_ranges_to_array(media.played)));
}
/**
* @param {HTMLVideoElement | HTMLAudioElement} media
* @param {(seeking: boolean) => void} update
*/
export function bind_seeking(media, update) {
listen(media, ['seeking', 'seeked'], () => update(media.seeking));
}
/**
* @param {HTMLVideoElement | HTMLAudioElement} media
* @param {(seeking: boolean) => void} update
*/
export function bind_ended(media, update) {
listen(media, ['timeupdate', 'ended'], () => update(media.ended));
}
/**
* @param {HTMLVideoElement | HTMLAudioElement} media
* @param {(ready_state: number) => void} update
*/
export function bind_ready_state(media, update) {
listen(
media,
['loadedmetadata', 'loadeddata', 'canplay', 'canplaythrough', 'playing', 'waiting', 'emptied'],
() => update(media.readyState)
);
}
/**
* @param {HTMLVideoElement | HTMLAudioElement} media
* @param {() => number | undefined} get_value
* @param {(playback_rate: number) => void} update
*/
export function bind_playback_rate(media, get_value, update) {
var updating = false;
var callback = () => {
if (!updating) {
update(media.playbackRate);
}
updating = false;
};
// Needs to happen after the element is inserted into the dom, else playback will be set back to 1 by the browser.
// For hydration we could do it immediately but the additional code is not worth the lost microtask.
/** @type {import('#client').Effect | undefined} */
var render;
var destroyed = false;
var effect = managed_effect(() => {
destroy_effect(effect);
if (destroyed) return;
if (get_value() == null) {
callback();
}
listen(media, ['ratechange'], callback, false);
render = render_effect(() => {
var value = get_value();
// through isNaN we also allow number strings, which is more robust
if (!isNaN(/** @type {any} */ (value)) && value !== media.playbackRate) {
updating = true;
media.playbackRate = /** @type {number} */ (value);
}
});
});
render_effect(() => () => {
destroyed = true;
if (render) {
destroy_effect(render);
}
});
}
/**
* @param {HTMLVideoElement | HTMLAudioElement} media
* @param {() => boolean | undefined} get_value
* @param {(paused: boolean) => void} update
*/
export function bind_paused(media, get_value, update) {
var mounted = hydrating;
var paused = get_value();
var callback = () => {
if (paused !== media.paused) {
paused = media.paused;
update((paused = media.paused));
}
};
if (paused == null) {
callback();
}
// Defer listening if not mounted yet so that the first canplay event doesn't cause a potentially wrong update
if (mounted) {
// If someone switches the src while media is playing, the player will pause.
// Listen to the canplay event to get notified of this situation.
listen(media, ['play', 'pause', 'canplay'], callback, false);
}
render_effect(() => {
paused = !!get_value();
if (paused !== media.paused) {
var toggle = () => {
mounted = true;
if (paused) {
media.pause();
} else {
media.play().catch(() => {
update((paused = true));
});
}
};
if (mounted) {
toggle();
} else {
// If this is the first invocation in dom mode, the media element isn't mounted yet,
// and therefore its resource isn't loaded yet. We need to wait for the canplay event
// in this case or else we'll get a "The play() request was interrupted by a new load request" error.
media.addEventListener(
'canplay',
() => {
listen(media, ['play', 'pause', 'canplay'], callback, false);
toggle();
},
{ once: true }
);
}
}
});
}
/**
* @param {HTMLVideoElement | HTMLAudioElement} media
* @param {() => number | undefined} get_value
* @param {(volume: number) => void} update
*/
export function bind_volume(media, get_value, update) {
var updating = false;
var callback = () => {
updating = true;
update(media.volume);
};
if (get_value() == null) {
callback();
}
listen(media, ['volumechange'], callback, false);
render_effect(() => {
var value = get_value();
// through isNaN we also allow number strings, which is more robust
if (!updating && !isNaN(/** @type {any} */ (value))) {
media.volume = /** @type {number} */ (value);
}
updating = false;
});
}
/**
* @param {HTMLVideoElement | HTMLAudioElement} media
* @param {() => boolean | undefined} get_value
* @param {(muted: boolean) => void} update
*/
export function bind_muted(media, get_value, update) {
var updating = false;
var callback = () => {
updating = true;
update(media.muted);
};
if (get_value() == null) {
callback();
}
listen(media, ['volumechange'], callback, false);
render_effect(() => {
var value = get_value();
if (!updating) media.muted = !!value;
updating = false;
});
}

@ -0,0 +1,11 @@
import { listen } from './shared.js';
/**
* @param {(online: boolean) => void} update
* @returns {void}
*/
export function bind_online(update) {
listen(window, ['online', 'offline'], () => {
update(navigator.onLine);
});
}

@ -0,0 +1,22 @@
import { render_effect } from '../../../reactivity/effects.js';
import { get_descriptor } from '../../../utils.js';
/**
* Makes an `export`ed (non-prop) variable available on the `$$props` object
* so that consumers can do `bind:x` on the component.
* @template V
* @param {Record<string, unknown>} props
* @param {string} prop
* @param {V} value
* @returns {void}
*/
export function bind_prop(props, prop, value) {
var desc = get_descriptor(props, prop);
if (desc && desc.set) {
props[prop] = value;
render_effect(() => () => {
props[prop] = null;
});
}
}

@ -0,0 +1,116 @@
import { effect } from '../../../reactivity/effects.js';
/**
* Selects the correct option(s) (depending on whether this is a multiple select)
* @template V
* @param {HTMLSelectElement} select
* @param {V} value
* @param {boolean} [mounting]
*/
export function select_option(select, value, mounting) {
if (select.multiple) {
return select_options(select, value);
}
for (var option of select.options) {
var option_value = get_option_value(option);
if (option_value === value) {
option.selected = true;
return;
}
}
if (!mounting || value !== undefined) {
select.selectedIndex = -1; // no option should be selected
}
}
/**
* Finds the containing `<select>` element and potentially updates its `selected` state.
* @param {HTMLOptionElement} option
* @returns {void}
*/
export function selected(option) {
// Inside an effect because the element might not be connected
// to the parent <select> yet when this is called
effect(() => {
var select = option.parentNode;
while (select != null) {
if (select.nodeName === 'SELECT') break;
select = select.parentNode;
}
// @ts-ignore
if (select != null && option.__value === select.__value) {
// never set to false, since this causes browser to select default option
option.selected = true;
}
});
}
/**
* @param {HTMLSelectElement} select
* @param {() => unknown} get_value
* @param {(value: unknown) => void} update
* @returns {void}
*/
export function bind_select_value(select, get_value, update) {
var mounting = true;
select.addEventListener('change', () => {
/** @type {unknown} */
var value;
if (select.multiple) {
value = [].map.call(select.querySelectorAll(':checked'), get_option_value);
} else {
/** @type {HTMLOptionElement | null} */
var selected_option = select.querySelector(':checked');
value = selected_option && get_option_value(selected_option);
}
update(value);
});
// Needs to be an effect, not a render_effect, so that in case of each loops the logic runs after the each block has updated
effect(() => {
var value = get_value();
select_option(select, value, mounting);
if (mounting && value === undefined) {
/** @type {HTMLOptionElement | null} */
var selected_option = select.querySelector(':checked');
if (selected_option !== null) {
value = get_option_value(selected_option);
update(value);
}
}
// @ts-ignore
select.__value = value;
mounting = false;
});
}
/**
* @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 <option> has a value attribute
if ('__value' in option) {
return option.__value;
} else {
return option.value;
}
}

@ -0,0 +1,27 @@
import { render_effect } from '../../../reactivity/effects.js';
/**
* Fires the handler once immediately (unless corresponding arg is set to `false`),
* then listens to the given events until the render effect context is destroyed
* @param {Element | Window} target
* @param {Array<string>} events
* @param {() => void} handler
* @param {any} call_handler_immediately
*/
export function listen(target, events, handler, call_handler_immediately = true) {
if (call_handler_immediately) {
handler();
}
for (var name of events) {
target.addEventListener(name, handler);
}
render_effect(() => {
return () => {
for (var name of events) {
target.removeEventListener(name, handler);
}
};
});
}

@ -0,0 +1,107 @@
import { effect, render_effect } from '../../../reactivity/effects.js';
import { untrack } from '../../../runtime.js';
/**
* Resize observer singleton.
* One listener per element only!
* https://groups.google.com/a/chromium.org/g/blink-dev/c/z6ienONUb5A/m/F5-VcUZtBAAJ
*/
class ResizeObserverSingleton {
/** */
#listeners = new WeakMap();
/** @type {ResizeObserver | undefined} */
#observer;
/** @type {ResizeObserverOptions} */
#options;
/** @static */
static entries = new WeakMap();
/** @param {ResizeObserverOptions} options */
constructor(options) {
this.#options = options;
}
/**
* @param {Element} element
* @param {(entry: ResizeObserverEntry) => any} listener
*/
observe(element, listener) {
var listeners = this.#listeners.get(element) || new Set();
listeners.add(listener);
this.#listeners.set(element, listeners);
this.#getObserver().observe(element, this.#options);
return () => {
var listeners = this.#listeners.get(element);
listeners.delete(listener);
if (listeners.size === 0) {
this.#listeners.delete(element);
/** @type {ResizeObserver} */ (this.#observer).unobserve(element);
}
};
}
#getObserver() {
return (
this.#observer ??
(this.#observer = new ResizeObserver(
/** @param {any} entries */ (entries) => {
for (var entry of entries) {
ResizeObserverSingleton.entries.set(entry.target, entry);
for (var listener of this.#listeners.get(entry.target) || []) {
listener(entry);
}
}
}
))
);
}
}
var resize_observer_content_box = /* @__PURE__ */ new ResizeObserverSingleton({
box: 'content-box'
});
var resize_observer_border_box = /* @__PURE__ */ new ResizeObserverSingleton({
box: 'border-box'
});
var resize_observer_device_pixel_content_box = /* @__PURE__ */ new ResizeObserverSingleton({
box: 'device-pixel-content-box'
});
/**
* @param {Element} element
* @param {'contentRect' | 'contentBoxSize' | 'borderBoxSize' | 'devicePixelContentBoxSize'} type
* @param {(entry: keyof ResizeObserverEntry) => void} update
*/
export function bind_resize_observer(element, type, update) {
var observer =
type === 'contentRect' || type === 'contentBoxSize'
? resize_observer_content_box
: type === 'borderBoxSize'
? resize_observer_border_box
: resize_observer_device_pixel_content_box;
var unsub = observer.observe(element, /** @param {any} entry */ (entry) => update(entry[type]));
render_effect(() => unsub);
}
/**
* @param {HTMLElement} element
* @param {'clientWidth' | 'clientHeight' | 'offsetWidth' | 'offsetHeight'} type
* @param {(size: number) => void} update
*/
export function bind_element_size(element, type, update) {
var unsub = resize_observer_border_box.observe(element, () => update(element[type]));
effect(() => {
untrack(() => update(element[type]));
return unsub;
});
}

@ -0,0 +1,60 @@
import { STATE_SYMBOL } from '../../../constants.js';
import { effect } from '../../../reactivity/effects.js';
import { untrack } from '../../../runtime.js';
/**
* @param {any} bound_value
* @param {Element} element_or_component
* @returns {boolean}
*/
function is_bound_this(bound_value, element_or_component) {
// Find the original target if the value is proxied.
var proxy_target = bound_value && bound_value[STATE_SYMBOL]?.t;
return bound_value === element_or_component || proxy_target === element_or_component;
}
/**
* @param {Element} element_or_component
* @param {(value: unknown, ...parts: unknown[]) => void} update
* @param {(...parts: unknown[]) => unknown} get_value
* @param {() => unknown[]} [get_parts] Set if the this binding is used inside an each block,
* returns all the parts of the each block context that are used in the expression
* @returns {void}
*/
export function bind_this(element_or_component, update, get_value, get_parts) {
/** @type {unknown[]} */
var old_parts;
/** @type {unknown[]} */
var parts;
var e = effect(() => {
old_parts = parts;
// We only track changes to the parts, not the value itself to avoid unnecessary reruns.
parts = get_parts?.() || [];
untrack(() => {
if (element_or_component !== get_value(...parts)) {
update(element_or_component, ...parts);
// If this is an effect rerun (cause: each block context changes), then nullfiy the binding at
// the previous position if it isn't already taken over by a different effect.
if (old_parts && is_bound_this(get_value(...old_parts), element_or_component)) {
update(null, ...old_parts);
}
}
});
});
// Add effect teardown (likely causes: if block became false, each item removed, component unmounted).
// In these cases we need to nullify the binding only if we detect that the value is still the same.
// If not, that means that another effect has now taken over the binding.
e.ondestroy = () => {
// Defer to the next tick so that all updates can be reconciled first.
// This solves the case where one variable is shared across multiple this-bindings.
effect(() => {
if (parts && is_bound_this(get_value(...parts), element_or_component)) {
update(null, ...parts);
}
});
};
}

@ -0,0 +1,69 @@
import { render_effect } from '../../../reactivity/effects.js';
/**
* @param {'innerHTML' | 'textContent' | 'innerText'} property
* @param {HTMLElement} element
* @param {() => unknown} get_value
* @param {(value: unknown) => void} update
* @returns {void}
*/
export function bind_content_editable(property, element, get_value, update) {
element.addEventListener('input', () => {
// @ts-ignore
update(element[property]);
});
render_effect(() => {
var value = get_value();
if (element[property] !== value) {
if (value === null) {
// @ts-ignore
var non_null_value = element[property];
update(non_null_value);
} else {
// @ts-ignore
element[property] = value + '';
}
}
});
}
/**
* @param {string} property
* @param {string} event_name
* @param {'get' | 'set'} type
* @param {Element} element
* @param {() => unknown} get_value
* @param {(value: unknown) => void} update
* @returns {void}
*/
export function bind_property(property, event_name, type, element, get_value, update) {
var target_handler = () => {
// @ts-ignore
update(element[property]);
};
element.addEventListener(event_name, target_handler);
if (type === 'set') {
render_effect(() => {
// @ts-ignore
element[property] = get_value();
});
}
if (type === 'get') {
// @ts-ignore
update(element[property]);
}
render_effect(() => {
// @ts-ignore
if (element === document.body || element === window || element === document) {
return () => {
element.removeEventListener(event_name, target_handler);
};
}
});
}

@ -0,0 +1,61 @@
import { render_effect } from '../../../reactivity/effects.js';
import { listen } from './shared.js';
/**
* @param {'x' | 'y'} type
* @param {() => number} get_value
* @param {(value: number) => void} update
* @returns {void}
*/
export function bind_window_scroll(type, get_value, update) {
var is_scrolling_x = type === 'x';
var target_handler = () => {
scrolling = true;
clearTimeout(timeout);
timeout = setTimeout(clear, 100); // TODO use scrollend event if supported (or when supported everywhere?)
update(window[is_scrolling_x ? 'scrollX' : 'scrollY']);
};
addEventListener('scroll', target_handler, {
passive: true
});
var latest_value = 0;
var scrolling = false;
/** @type {ReturnType<typeof setTimeout>} */
var timeout;
var clear = () => {
scrolling = false;
};
render_effect(() => {
latest_value = get_value() || 0;
if (!scrolling) {
scrolling = true;
clearTimeout(timeout);
if (is_scrolling_x) {
scrollTo(latest_value, window.scrollY);
} else {
scrollTo(window.scrollX, latest_value);
}
timeout = setTimeout(clear, 100);
}
});
render_effect(() => {
return () => {
removeEventListener('scroll', target_handler);
};
});
}
/**
* @param {'innerWidth' | 'innerHeight' | 'outerWidth' | 'outerHeight'} type
* @param {(size: number) => void} update
*/
export function bind_window_size(type, update) {
listen(window, ['resize'], () => update(window[type]));
}

@ -0,0 +1,78 @@
import { hydrating } from '../hydration.js';
import { set_class_name } from '../operations.js';
import { render_effect } from '../../reactivity/effects.js';
/**
* @param {Element} dom
* @param {() => string} value
* @returns {void}
*/
export function class_name_effect(dom, value) {
render_effect(() => {
class_name(dom, value());
});
}
/**
* @param {Element} dom
* @param {string} value
* @returns {void}
*/
export function class_name(dom, value) {
// @ts-expect-error need to add __className to patched prototype
var prev_class_name = dom.__className;
var next_class_name = to_class(value);
if (hydrating && dom.className === next_class_name) {
// In case of hydration don't reset the class as it's already correct.
// @ts-expect-error need to add __className to patched prototype
dom.__className = next_class_name;
} else if (
prev_class_name !== next_class_name ||
(hydrating && dom.className !== next_class_name)
) {
if (next_class_name === '') {
dom.removeAttribute('class');
} else {
set_class_name(dom, next_class_name);
}
// @ts-expect-error need to add __className to patched prototype
dom.__className = next_class_name;
}
}
/**
* @template V
* @param {V} value
* @returns {string | V}
*/
export function to_class(value) {
return value == null ? '' : value;
}
/**
* @param {Element} dom
* @param {string} class_name
* @param {boolean} value
* @returns {void}
*/
export function class_toggle(dom, class_name, value) {
if (value) {
dom.classList.add(class_name);
} else {
dom.classList.remove(class_name);
}
}
/**
* @param {Element} dom
* @param {string} class_name
* @param {() => boolean} value
* @returns {void}
*/
export function class_toggle_effect(dom, class_name, value) {
render_effect(() => {
class_toggle(dom, class_name, value());
});
}

@ -1,8 +1,7 @@
import { createClassComponent } from '../../legacy/legacy-client.js'; import { createClassComponent } from '../../../../legacy/legacy-client.js';
import { destroy_signal } from './runtime.js'; import { destroy_effect, render_effect } from '../../reactivity/effects.js';
import { render_effect } from './reactivity/computations.js'; import { open, close } from '../template.js';
import { open, close } from './render.js'; import { define_property } from '../../utils.js';
import { define_property } from './utils.js';
/** /**
* @typedef {Object} CustomElementPropDefinition * @typedef {Object} CustomElementPropDefinition
@ -199,7 +198,7 @@ if (typeof HTMLElement === 'function') {
Promise.resolve().then(() => { Promise.resolve().then(() => {
if (!this.$$cn) { if (!this.$$cn) {
this.$$c.$destroy(); this.$$c.$destroy();
destroy_signal(this.$$me); destroy_effect(this.$$me);
this.$$c = undefined; this.$$c = undefined;
} }
}); });

@ -0,0 +1,154 @@
import { render_effect } from '../../reactivity/effects.js';
import { all_registered_events, root_event_handles } from '../../render.js';
import { define_property, is_array } from '../../utils.js';
/**
* @param {string} event_name
* @param {Element} dom
* @param {EventListener} handler
* @param {boolean} capture
* @param {boolean} [passive]
* @returns {void}
*/
export function event(event_name, dom, handler, capture, passive) {
var options = { capture, passive };
/**
* @this {EventTarget}
*/
function target_handler(/** @type {Event} */ event) {
handle_event_propagation(dom, event);
if (!event.cancelBubble) {
return handler.call(this, event);
}
}
dom.addEventListener(event_name, target_handler, options);
// @ts-ignore
if (dom === document.body || dom === window || dom === document) {
render_effect(() => {
return () => {
dom.removeEventListener(event_name, target_handler, options);
};
});
}
}
/**
* @param {Array<string>} events
* @returns {void}
*/
export function delegate(events) {
for (var i = 0; i < events.length; i++) {
all_registered_events.add(events[i]);
}
for (var fn of root_event_handles) {
fn(events);
}
}
/**
* @param {Node} handler_element
* @param {Event} event
* @returns {void}
*/
export function handle_event_propagation(handler_element, event) {
var owner_document = handler_element.ownerDocument;
var event_name = event.type;
var path = event.composedPath?.() || [];
var current_target = /** @type {null | Element} */ (path[0] || event.target);
if (event.target !== current_target) {
define_property(event, 'target', {
configurable: true,
value: current_target
});
}
// composedPath contains list of nodes the event has propagated through.
// We check __root to skip all nodes below it in case this is a
// parent of the __root node, which indicates that there's nested
// mounted apps. In this case we don't want to trigger events multiple times.
var path_idx = 0;
// @ts-expect-error is added below
var handled_at = event.__root;
if (handled_at) {
var at_idx = path.indexOf(handled_at);
if (
at_idx !== -1 &&
(handler_element === document || handler_element === /** @type {any} */ (window))
) {
// This is the fallback document listener or a window listener, but the event was already handled
// -> ignore, but set handle_at to document/window so that we're resetting the event
// chain in case someone manually dispatches the same event object again.
// @ts-expect-error
event.__root = handler_element;
return;
}
// We're deliberately not skipping if the index is higher, because
// someone could create an event programmatically and emit it multiple times,
// in which case we want to handle the whole propagation chain properly each time.
// (this will only be a false negative if the event is dispatched multiple times and
// the fallback document listener isn't reached in between, but that's super rare)
var handler_idx = path.indexOf(handler_element);
if (handler_idx === -1) {
// handle_idx can theoretically be -1 (happened in some JSDOM testing scenarios with an event listener on the window object)
// so guard against that, too, and assume that everything was handled at this point.
return;
}
if (at_idx <= handler_idx) {
// +1 because at_idx is the element which was already handled, and there can only be one delegated event per element.
// Avoids on:click and onclick on the same event resulting in onclick being fired twice.
path_idx = at_idx + 1;
}
}
current_target = /** @type {Element} */ (path[path_idx] || event.target);
// Proxy currentTarget to correct target
define_property(event, 'currentTarget', {
configurable: true,
get() {
return current_target || owner_document;
}
});
while (current_target !== null) {
/** @type {null | Element} */
var parent_element =
current_target.parentNode || /** @type {any} */ (current_target).host || null;
var internal_prop_name = '__' + event_name;
// @ts-ignore
var delegated = current_target[internal_prop_name];
if (delegated !== undefined && !(/** @type {any} */ (current_target).disabled)) {
if (is_array(delegated)) {
var [fn, ...data] = delegated;
fn.apply(current_target, [event, ...data]);
} else {
delegated.call(current_target, event);
}
}
if (
event.cancelBubble ||
parent_element === handler_element ||
current_target === handler_element
) {
break;
}
current_target = parent_element;
}
// @ts-expect-error is used above
event.__root = handler_element;
// @ts-expect-error is used above
current_target = handler_element;
}

@ -0,0 +1,37 @@
import { hydrating } from '../hydration.js';
import { render_effect } from '../../reactivity/effects.js';
import { current_block } from '../../runtime.js';
/**
* @param {HTMLElement} dom
* @param {boolean} value
* @returns {void}
*/
export function autofocus(dom, value) {
if (value) {
const body = document.body;
dom.autofocus = true;
render_effect(
() => {
if (document.activeElement === body) {
dom.focus();
}
},
current_block,
true,
false
);
}
}
/**
* The child of a textarea actually corresponds to the defaultValue property, so we need
* to remove it upon hydration to avoid a bug when someone resets the form value.
* @param {HTMLTextAreaElement} dom
* @returns {void}
*/
export function remove_textarea_child(dom) {
if (hydrating && dom.firstChild !== null) {
dom.textContent = '';
}
}

@ -0,0 +1,33 @@
import { render_effect } from '../../reactivity/effects.js';
/**
* @param {HTMLElement} dom
* @param {string} key
* @param {string} value
* @param {boolean} [important]
*/
export function style(dom, key, value, important) {
const style = dom.style;
const prev_value = style.getPropertyValue(key);
if (value == null) {
if (prev_value !== '') {
style.removeProperty(key);
}
} else if (prev_value !== value) {
style.setProperty(key, value, important ? 'important' : '');
}
}
/**
* @param {HTMLElement} dom
* @param {string} key
* @param {() => string} value
* @param {boolean} [important]
* @returns {void}
*/
export function style_effect(dom, key, value, important) {
render_effect(() => {
const string = value();
style(dom, key, string, important);
});
}

@ -1,5 +1,5 @@
import { EACH_IS_ANIMATED, EACH_IS_CONTROLLED } from '../../constants.js'; import { EACH_IS_ANIMATED, EACH_IS_CONTROLLED } from '../../../../constants.js';
import { run_all } from '../common.js'; import { run_all } from '../../../common.js';
import { import {
AWAIT_BLOCK, AWAIT_BLOCK,
DYNAMIC_COMPONENT_BLOCK, DYNAMIC_COMPONENT_BLOCK,
@ -8,20 +8,24 @@ import {
IF_BLOCK, IF_BLOCK,
KEY_BLOCK, KEY_BLOCK,
ROOT_BLOCK ROOT_BLOCK
} from './constants.js'; } from '../../constants.js';
import { destroy_each_item_block, get_first_element } from './dom/blocks/each.js'; import { destroy_each_item_block, get_first_element } from '../blocks/each.js';
import { schedule_raf_task } from './dom/task.js'; import { schedule_raf_task } from '../task.js';
import { append_child, empty } from './operations.js'; import { append_child, empty } from '../operations.js';
import { effect, managed_effect, managed_pre_effect } from './reactivity/computations.js'; import {
destroy_effect,
effect,
managed_effect,
managed_pre_effect
} from '../../reactivity/effects.js';
import { import {
current_block, current_block,
current_effect, current_effect,
destroy_signal,
execute_effect, execute_effect,
mark_subtree_inert, mark_subtree_inert,
untrack untrack
} from './runtime.js'; } from '../../runtime.js';
import { raf } from './timing.js'; import { raf } from '../../timing.js';
const active_tick_animations = new Set(); const active_tick_animations = new Set();
const DELAY_NEXT_TICK = Number.MIN_SAFE_INTEGER; const DELAY_NEXT_TICK = Number.MIN_SAFE_INTEGER;
@ -29,6 +33,54 @@ const DELAY_NEXT_TICK = Number.MIN_SAFE_INTEGER;
/** @type {undefined | number} */ /** @type {undefined | number} */
let active_tick_ref = undefined; let active_tick_ref = undefined;
/**
* @template P
* @param {HTMLElement} dom
* @param {() => import('#client').TransitionFn<P | undefined>} get_transition_fn
* @param {(() => P) | null} props
* @param {any} global
* @returns {void}
*/
export function transition(dom, get_transition_fn, props, global = false) {
bind_transition(dom, get_transition_fn, props, 'both', global);
}
/**
* @template P
* @param {HTMLElement} dom
* @param {() => import('#client').TransitionFn<P | undefined>} get_transition_fn
* @param {(() => P) | null} props
* @returns {void}
*/
export function animate(dom, get_transition_fn, props) {
bind_transition(dom, get_transition_fn, props, 'key', false);
}
/**
* @template P
* @param {HTMLElement} dom
* @param {() => import('#client').TransitionFn<P | undefined>} get_transition_fn
* @param {(() => P) | null} props
* @param {any} global
* @returns {void}
*/
function in_fn(dom, get_transition_fn, props, global = false) {
bind_transition(dom, get_transition_fn, props, 'in', global);
}
export { in_fn as in };
/**
* @template P
* @param {HTMLElement} dom
* @param {() => import('#client').TransitionFn<P | undefined>} get_transition_fn
* @param {(() => P) | null} props
* @param {any} global
* @returns {void}
*/
export function out(dom, get_transition_fn, props, global = false) {
bind_transition(dom, get_transition_fn, props, 'out', global);
}
/** /**
* @template T * @template T
* @param {string} type * @param {string} type
@ -293,10 +345,10 @@ const linear = (t) => t;
/** /**
* @param {HTMLElement} dom * @param {HTMLElement} dom
* @param {() => import('./types.js').TransitionPayload} init * @param {() => import('../../types.js').TransitionPayload} init
* @param {'in' | 'out' | 'both' | 'key'} direction * @param {'in' | 'out' | 'both' | 'key'} direction
* @param {import('./types.js').EffectSignal} effect * @param {import('../../types.js').Effect} effect
* @returns {import('./types.js').Transition} * @returns {import('../../types.js').Transition}
*/ */
function create_transition(dom, init, direction, effect) { function create_transition(dom, init, direction, effect) {
let curr_direction = 'in'; let curr_direction = 'in';
@ -309,7 +361,7 @@ function create_transition(dom, init, direction, effect) {
let cancelled = false; let cancelled = false;
const create_animation = () => { const create_animation = () => {
let payload = /** @type {import('./types.js').TransitionPayload} */ (transition.p); let payload = /** @type {import('../../types.js').TransitionPayload} */ (transition.p);
if (typeof payload === 'function') { if (typeof payload === 'function') {
// @ts-ignore // @ts-ignore
payload = payload({ direction: curr_direction }); payload = payload({ direction: curr_direction });
@ -350,7 +402,7 @@ function create_transition(dom, init, direction, effect) {
}; };
}; };
/** @type {import('./types.js').Transition} */ /** @type {import('../../types.js').Transition} */
const transition = { const transition = {
e: effect, e: effect,
i: init, i: init,
@ -478,7 +530,7 @@ function create_transition(dom, init, direction, effect) {
} }
/** /**
* @param {import('./types.js').Block} block * @param {import('../../types.js').Block} block
* @returns {boolean} * @returns {boolean}
*/ */
function is_transition_block(block) { function is_transition_block(block) {
@ -496,14 +548,14 @@ function is_transition_block(block) {
/** /**
* @template P * @template P
* @param {HTMLElement} dom * @param {HTMLElement} dom
* @param {() => import('./types.js').TransitionFn<P | undefined> | import('./types.js').AnimateFn<P | undefined>} get_transition_fn * @param {() => import('../../types.js').TransitionFn<P | undefined> | import('../../types.js').AnimateFn<P | undefined>} get_transition_fn
* @param {(() => P) | null} props_fn * @param {(() => P) | null} props_fn
* @param {'in' | 'out' | 'both' | 'key'} direction * @param {'in' | 'out' | 'both' | 'key'} direction
* @param {boolean} global * @param {boolean} global
* @returns {void} * @returns {void}
*/ */
export function bind_transition(dom, get_transition_fn, props_fn, direction, global) { function bind_transition(dom, get_transition_fn, props_fn, direction, global) {
const transition_effect = /** @type {import('./types.js').EffectSignal} */ (current_effect); const transition_effect = /** @type {import('../../types.js').Effect} */ (current_effect);
const block = current_block; const block = current_block;
const is_keyed_transition = direction === 'key'; const is_keyed_transition = direction === 'key';
@ -514,7 +566,7 @@ export function bind_transition(dom, get_transition_fn, props_fn, direction, glo
// @ts-ignore // @ts-ignore
dom.__animate = true; dom.__animate = true;
} }
/** @type {import('./types.js').Block | null} */ /** @type {import('../../types.js').Block | null} */
let transition_block = block; let transition_block = block;
main: while (transition_block !== null) { main: while (transition_block !== null) {
if (is_transition_block(transition_block)) { if (is_transition_block(transition_block)) {
@ -528,7 +580,7 @@ export function bind_transition(dom, get_transition_fn, props_fn, direction, glo
} else if (transition_block.t === IF_BLOCK) { } else if (transition_block.t === IF_BLOCK) {
transition_block.r = if_block_transition; transition_block.r = if_block_transition;
if (can_show_intro_on_mount) { if (can_show_intro_on_mount) {
/** @type {import('./types.js').Block | null} */ /** @type {import('../../types.js').Block | null} */
let if_block = transition_block; let if_block = transition_block;
while (if_block.t === IF_BLOCK) { while (if_block.t === IF_BLOCK) {
// If we have an if block parent that is currently falsy then // If we have an if block parent that is currently falsy then
@ -553,7 +605,7 @@ export function bind_transition(dom, get_transition_fn, props_fn, direction, glo
transition_block = transition_block.p; transition_block = transition_block.p;
} }
/** @type {import('./types.js').Transition} */ /** @type {import('../../types.js').Transition} */
let transition; let transition;
effect(() => { effect(() => {
@ -569,13 +621,13 @@ export function bind_transition(dom, get_transition_fn, props_fn, direction, glo
untrack(() => { untrack(() => {
const props = props_fn === null ? {} : props_fn(); const props = props_fn === null ? {} : props_fn();
return is_keyed_transition return is_keyed_transition
? /** @type {import('./types.js').AnimateFn<any>} */ (transition_fn)( ? /** @type {import('../../types.js').AnimateFn<any>} */ (transition_fn)(
dom, dom,
{ from: /** @type {DOMRect} */ (from), to: dom.getBoundingClientRect() }, { from: /** @type {DOMRect} */ (from), to: dom.getBoundingClientRect() },
props, props,
{} {}
) )
: /** @type {import('./types.js').TransitionFn<any>} */ (transition_fn)(dom, props, { : /** @type {import('../../types.js').TransitionFn<any>} */ (transition_fn)(dom, props, {
direction direction
}); });
}); });
@ -589,14 +641,14 @@ export function bind_transition(dom, get_transition_fn, props_fn, direction, glo
} }
const effect = managed_pre_effect(() => { const effect = managed_pre_effect(() => {
destroy_signal(effect); destroy_effect(effect);
dom.inert = false; dom.inert = false;
if (show_intro && !already_mounted) { if (show_intro && !already_mounted) {
transition.in(); transition.in();
} }
/** @type {import('./types.js').Block | null} */ /** @type {import('../../types.js').Block | null} */
let transition_block = block; let transition_block = block;
while (!is_intro && transition_block !== null) { while (!is_intro && transition_block !== null) {
const parent = transition_block.p; const parent = transition_block.p;
@ -613,7 +665,7 @@ export function bind_transition(dom, get_transition_fn, props_fn, direction, glo
} }
transition_block = parent; transition_block = parent;
} }
}, false); });
}); });
if (direction === 'key') { if (direction === 'key') {
@ -626,7 +678,7 @@ export function bind_transition(dom, get_transition_fn, props_fn, direction, glo
} }
/** /**
* @param {Set<import('./types.js').Transition>} transitions * @param {Set<import('../../types.js').Transition>} transitions
* @param {'in' | 'out' | 'key'} target_direction * @param {'in' | 'out' | 'key'} target_direction
* @param {DOMRect} [from] * @param {DOMRect} [from]
* @returns {void} * @returns {void}
@ -666,18 +718,18 @@ export function trigger_transitions(transitions, target_direction, from) {
if (outros.length > 0) { if (outros.length > 0) {
// Defer the outros to a microtask // Defer the outros to a microtask
const e = managed_pre_effect(() => { const e = managed_pre_effect(() => {
destroy_signal(e); destroy_effect(e);
const e2 = managed_effect(() => { const e2 = managed_effect(() => {
destroy_signal(e2); destroy_effect(e2);
run_all(outros); run_all(outros);
}); });
}, false); });
} }
} }
/** /**
* @this {import('./types.js').IfBlock} * @this {import('../../types.js').IfBlock}
* @param {import('./types.js').Transition} transition * @param {import('../../types.js').Transition} transition
* @returns {void} * @returns {void}
*/ */
function if_block_transition(transition) { function if_block_transition(transition) {
@ -687,32 +739,32 @@ function if_block_transition(transition) {
const consequent_transitions = (block.c ??= new Set()); const consequent_transitions = (block.c ??= new Set());
consequent_transitions.add(transition); consequent_transitions.add(transition);
transition.f(() => { transition.f(() => {
const c = /** @type {Set<import('./types.js').Transition>} */ (consequent_transitions); const c = /** @type {Set<import('../../types.js').Transition>} */ (consequent_transitions);
c.delete(transition); c.delete(transition);
// If the block has changed to falsy and has transitions // If the block has changed to falsy and has transitions
if (!block.v && c.size === 0) { if (!block.v && c.size === 0) {
const consequent_effect = block.ce; const consequent_effect = block.ce;
execute_effect(/** @type {import('./types.js').EffectSignal} */ (consequent_effect)); execute_effect(/** @type {import('../../types.js').Effect} */ (consequent_effect));
} }
}); });
} else { } else {
const alternate_transitions = (block.a ??= new Set()); const alternate_transitions = (block.a ??= new Set());
alternate_transitions.add(transition); alternate_transitions.add(transition);
transition.f(() => { transition.f(() => {
const a = /** @type {Set<import('./types.js').Transition>} */ (alternate_transitions); const a = /** @type {Set<import('../../types.js').Transition>} */ (alternate_transitions);
a.delete(transition); a.delete(transition);
// If the block has changed to truthy and has transitions // If the block has changed to truthy and has transitions
if (block.v && a.size === 0) { if (block.v && a.size === 0) {
const alternate_effect = block.ae; const alternate_effect = block.ae;
execute_effect(/** @type {import('./types.js').EffectSignal} */ (alternate_effect)); execute_effect(/** @type {import('../../types.js').Effect} */ (alternate_effect));
} }
}); });
} }
} }
/** /**
* @this {import('./types.js').EachItemBlock} * @this {import('../../types.js').EachItemBlock}
* @param {import('./types.js').Transition} transition * @param {import('../../types.js').Transition} transition
* @returns {void} * @returns {void}
*/ */
function each_item_transition(transition) { function each_item_transition(transition) {
@ -750,8 +802,8 @@ function each_item_transition(transition) {
/** /**
* *
* @param {import('./types.js').EachItemBlock} block * @param {import('../../types.js').EachItemBlock} block
* @param {Set<import('./types.js').Transition>} transitions * @param {Set<import('../../types.js').Transition>} transitions
*/ */
function each_item_animate(block, transitions) { function each_item_animate(block, transitions) {
const from_dom = /** @type {Element} */ (get_first_element(block)); const from_dom = /** @type {Element} */ (get_first_element(block));

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

Loading…
Cancel
Save