Merge branch 'main' into snippet-work

pull/10800/head
Simon Holthausen 1 year ago
commit 08f1441585

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: add `anchor` support to mount() API

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: improve unowned derived signal heuristics

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: improved effect sequencing and execution order

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: further improvements to effect scheduling and flushing

@ -0,0 +1,5 @@
---
'svelte': patch
---
breaking: onDestroy functions run child-first

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: ensure correct context for action update/destroy functions

@ -0,0 +1,5 @@
---
'svelte': patch
---
feat: more efficient hydration markers

@ -0,0 +1,5 @@
---
'svelte': patch
---
feat: use implicit return for each block keys

@ -0,0 +1,5 @@
---
'svelte': patch
---
breaking: always run pre effects immediately

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: ensure effect cleanup functions are called with null `this`

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: correctly handle closure passed to $derived.by when destructuring

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: improve order of pre-effect execution

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: improve action support for nested $effect

@ -0,0 +1,5 @@
---
"svelte": patch
---
Add `name` to HTMLDetailsAttributes

@ -0,0 +1,5 @@
---
'svelte': patch
---
breaking: prevent unparenthesized sequence expressions in attributes

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: expose 'svelte/internal' to prevent Vite erroring on startup

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: reliably remove undefined attributes during hydration

@ -0,0 +1,5 @@
---
'svelte': patch
---
feat: more efficient if block compiler output

@ -0,0 +1,5 @@
---
'svelte': patch
---
breaking: move compiler.cjs to compiler/index.js

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: update type of `options.target`

@ -0,0 +1,5 @@
---
'svelte': patch
---
feat: re-export built-ins from `svelte/reactivity` on the server

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: ensure transition errors are not swallowed

@ -21,15 +21,18 @@
"big-eggs-flash", "big-eggs-flash",
"big-eyes-carry", "big-eyes-carry",
"big-geese-act", "big-geese-act",
"big-moons-occur",
"blue-ants-raise", "blue-ants-raise",
"blue-rules-juggle", "blue-rules-juggle",
"blue-timers-film", "blue-timers-film",
"brave-points-sleep", "brave-points-sleep",
"brave-shrimps-kiss", "brave-shrimps-kiss",
"brave-walls-destroy", "brave-walls-destroy",
"brave-walls-flow",
"breezy-carrots-flash", "breezy-carrots-flash",
"bright-peas-juggle", "bright-peas-juggle",
"bright-snakes-sing", "bright-snakes-sing",
"brown-houses-obey",
"brown-months-fry", "brown-months-fry",
"brown-spoons-boil", "brown-spoons-boil",
"calm-ravens-sneeze", "calm-ravens-sneeze",
@ -46,12 +49,14 @@
"cold-birds-own", "cold-birds-own",
"cold-masks-learn", "cold-masks-learn",
"cool-ants-leave", "cool-ants-leave",
"cool-peas-lick",
"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-buses-laugh",
"curvy-cups-cough", "curvy-cups-cough",
"curvy-flies-exercise",
"curvy-ties-shout", "curvy-ties-shout",
"cyan-flowers-destroy", "cyan-flowers-destroy",
"cyan-spies-grin", "cyan-spies-grin",
@ -62,6 +67,7 @@
"dry-clocks-grow", "dry-clocks-grow",
"dry-eggs-play", "dry-eggs-play",
"dry-eggs-retire", "dry-eggs-retire",
"dry-pillows-exist",
"dull-coins-vanish", "dull-coins-vanish",
"dull-mangos-wave", "dull-mangos-wave",
"dull-pots-add", "dull-pots-add",
@ -70,6 +76,7 @@
"eight-steaks-shout", "eight-steaks-shout",
"eighty-bikes-camp", "eighty-bikes-camp",
"eighty-days-cheat", "eighty-days-cheat",
"eleven-beers-yell",
"eleven-cycles-applaud", "eleven-cycles-applaud",
"empty-bags-heal", "empty-bags-heal",
"empty-bulldogs-exercise", "empty-bulldogs-exercise",
@ -82,6 +89,7 @@
"famous-knives-sneeze", "famous-knives-sneeze",
"famous-pants-pay", "famous-pants-pay",
"fast-weeks-clean", "fast-weeks-clean",
"few-clouds-shop",
"few-mugs-fail", "few-mugs-fail",
"fifty-rice-wait", "fifty-rice-wait",
"fifty-steaks-float", "fifty-steaks-float",
@ -101,6 +109,7 @@
"friendly-lies-camp", "friendly-lies-camp",
"funny-wombats-argue", "funny-wombats-argue",
"fuzzy-bags-camp", "fuzzy-bags-camp",
"fuzzy-donuts-provide",
"gentle-dolls-juggle", "gentle-dolls-juggle",
"gentle-sheep-hug", "gentle-sheep-hug",
"gentle-spies-happen", "gentle-spies-happen",
@ -117,6 +126,7 @@
"green-eggs-approve", "green-eggs-approve",
"green-hounds-play", "green-hounds-play",
"green-tigers-judge", "green-tigers-judge",
"grumpy-jars-sparkle",
"happy-beds-scream", "happy-beds-scream",
"happy-suits-film", "happy-suits-film",
"healthy-planes-vanish", "healthy-planes-vanish",
@ -126,12 +136,14 @@
"honest-buses-add", "honest-buses-add",
"honest-dragons-turn", "honest-dragons-turn",
"honest-icons-change", "honest-icons-change",
"hot-jobs-tap",
"hungry-boxes-relate", "hungry-boxes-relate",
"hungry-dots-fry", "hungry-dots-fry",
"hungry-singers-share", "hungry-singers-share",
"hungry-tips-unite", "hungry-tips-unite",
"hungry-trees-travel", "hungry-trees-travel",
"itchy-beans-melt", "itchy-beans-melt",
"itchy-bulldogs-tan",
"itchy-kings-deliver", "itchy-kings-deliver",
"itchy-lions-wash", "itchy-lions-wash",
"itchy-terms-guess", "itchy-terms-guess",
@ -148,6 +160,7 @@
"large-clouds-carry", "large-clouds-carry",
"large-turkeys-deny", "large-turkeys-deny",
"late-crabs-lay", "late-crabs-lay",
"late-peaches-mate",
"lazy-masks-sit", "lazy-masks-sit",
"lazy-months-knock", "lazy-months-knock",
"lazy-spiders-think", "lazy-spiders-think",
@ -159,15 +172,21 @@
"little-pans-jog", "little-pans-jog",
"long-buckets-lay", "long-buckets-lay",
"long-crews-return", "long-crews-return",
"long-humans-repair",
"long-lobsters-mate", "long-lobsters-mate",
"loud-cheetahs-flow", "loud-cheetahs-flow",
"loud-mugs-smile",
"loud-ravens-drop", "loud-ravens-drop",
"lovely-carpets-lick", "lovely-carpets-lick",
"lovely-houses-own",
"lovely-items-turn", "lovely-items-turn",
"lovely-rules-eat", "lovely-rules-eat",
"lucky-schools-hang", "lucky-schools-hang",
"lucky-toes-begin", "lucky-toes-begin",
"many-rockets-give",
"many-trees-fix", "many-trees-fix",
"metal-clouds-raise",
"metal-lobsters-burn",
"mighty-cooks-scream", "mighty-cooks-scream",
"mighty-files-hammer", "mighty-files-hammer",
"moody-carrots-lay", "moody-carrots-lay",
@ -179,8 +198,10 @@
"nasty-yaks-peel", "nasty-yaks-peel",
"neat-boats-shake", "neat-boats-shake",
"neat-dingos-clap", "neat-dingos-clap",
"neat-files-rescue",
"nervous-spoons-relax", "nervous-spoons-relax",
"new-boats-wait", "new-boats-wait",
"new-brooms-grin",
"new-rabbits-flow", "new-rabbits-flow",
"nice-avocados-move", "nice-avocados-move",
"ninety-dingos-walk", "ninety-dingos-walk",
@ -223,8 +244,10 @@
"real-guests-do", "real-guests-do",
"real-items-suffer", "real-items-suffer",
"real-pandas-brush", "real-pandas-brush",
"red-cycles-pretend",
"red-doors-own", "red-doors-own",
"red-feet-worry", "red-feet-worry",
"red-poets-study",
"rich-cobras-exist", "rich-cobras-exist",
"rich-olives-yell", "rich-olives-yell",
"rich-sheep-burn", "rich-sheep-burn",
@ -239,8 +262,10 @@
"selfish-dragons-knock", "selfish-dragons-knock",
"selfish-spies-help", "selfish-spies-help",
"selfish-tools-hide", "selfish-tools-hide",
"serious-gorillas-eat",
"serious-kids-deliver", "serious-kids-deliver",
"serious-needles-joke", "serious-needles-joke",
"serious-poems-brake",
"serious-socks-cover", "serious-socks-cover",
"serious-zebras-scream", "serious-zebras-scream",
"seven-deers-jam", "seven-deers-jam",
@ -258,6 +283,8 @@
"short-countries-rush", "short-countries-rush",
"silent-apes-report", "silent-apes-report",
"silly-laws-happen", "silly-laws-happen",
"silly-lies-film",
"silly-ways-wash",
"silver-points-approve", "silver-points-approve",
"sixty-items-crash", "sixty-items-crash",
"slimy-clouds-talk", "slimy-clouds-talk",
@ -266,9 +293,12 @@
"slow-beds-shave", "slow-beds-shave",
"slow-chefs-dream", "slow-chefs-dream",
"slow-kids-sparkle", "slow-kids-sparkle",
"slow-plums-chew",
"slow-wombats-reply", "slow-wombats-reply",
"small-papayas-laugh", "small-papayas-laugh",
"small-sheep-type", "small-sheep-type",
"small-spiders-fail",
"smart-cherries-leave",
"smart-parents-swim", "smart-parents-swim",
"smart-turkeys-tell", "smart-turkeys-tell",
"smart-zebras-pay", "smart-zebras-pay",
@ -290,6 +320,7 @@
"spotty-turkeys-sparkle", "spotty-turkeys-sparkle",
"stale-books-perform", "stale-books-perform",
"stale-comics-look", "stale-comics-look",
"stale-fans-rest",
"stale-jeans-refuse", "stale-jeans-refuse",
"strange-apricots-happen", "strange-apricots-happen",
"strong-gifts-smoke", "strong-gifts-smoke",
@ -300,6 +331,7 @@
"swift-donkeys-perform", "swift-donkeys-perform",
"swift-fans-stare", "swift-fans-stare",
"swift-feet-juggle", "swift-feet-juggle",
"swift-poets-carry",
"swift-ravens-hunt", "swift-ravens-hunt",
"swift-seahorses-deliver", "swift-seahorses-deliver",
"tall-books-grin", "tall-books-grin",
@ -316,6 +348,7 @@
"ten-foxes-repeat", "ten-foxes-repeat",
"ten-jokes-divide", "ten-jokes-divide",
"ten-peaches-sleep", "ten-peaches-sleep",
"ten-singers-cough",
"ten-ties-repair", "ten-ties-repair",
"ten-worms-reflect", "ten-worms-reflect",
"thick-cycles-rule", "thick-cycles-rule",
@ -328,7 +361,9 @@
"thirty-pears-hug", "thirty-pears-hug",
"thirty-wombats-relax", "thirty-wombats-relax",
"three-camels-sell", "three-camels-sell",
"three-foxes-relax",
"three-icons-trade", "three-icons-trade",
"three-lions-visit",
"three-papayas-buy", "three-papayas-buy",
"three-suits-grin", "three-suits-grin",
"tidy-buses-whisper", "tidy-buses-whisper",
@ -351,6 +386,7 @@
"wicked-doors-train", "wicked-doors-train",
"wicked-hairs-cheer", "wicked-hairs-cheer",
"wild-foxes-wonder", "wild-foxes-wonder",
"wild-moose-compare",
"wise-apples-care", "wise-apples-care",
"wise-dancers-hang", "wise-dancers-hang",
"wise-dodos-tell", "wise-dodos-tell",

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: Add `elementtiming` HTMLAttribute, remove `crossorigin` from HTMLInputAttributes

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: allow runes for variable declarations in the template

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: correctly hydrate controlled each-else block

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: revert SSR shorthand comments

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: child effects are removed from parent branches

@ -0,0 +1,5 @@
---
"svelte": patch
---
feat: take form resets into account for two way bindings

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: handle multiple snippet parameters with one or more being optional

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: hydrate HTML with surrounding whitespace

@ -0,0 +1,5 @@
---
'svelte': patch
---
feat: shorter compiler output for attribute updates

@ -0,0 +1,5 @@
---
'svelte': patch
---
feat: faster HTML tags

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: Add `dirname` to HTMLInputAttributes

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: apply animate on prefix/suffix each block mutations

@ -0,0 +1,5 @@
---
'svelte': patch
---
feat: more efficient each block compiler output

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: include compiler/package.json in package

@ -11,6 +11,7 @@
**/vite.config.js **/vite.config.js
**/vite.prod.config.js **/vite.prod.config.js
**/node_modules **/node_modules
**/compiler/index.js
**/tests/** **/tests/**

@ -10,12 +10,10 @@ packages/svelte/tests/**/_actual*
packages/svelte/tests/**/expected* packages/svelte/tests/**/expected*
packages/svelte/tests/**/_output packages/svelte/tests/**/_output
packages/svelte/tests/**/shards/*.test.js packages/svelte/tests/**/shards/*.test.js
packages/svelte/tests/hydration/samples/*/_before.html packages/svelte/tests/hydration/samples/*/_expected.html
packages/svelte/tests/hydration/samples/*/_before_head.html packages/svelte/tests/hydration/samples/*/_override.html
packages/svelte/tests/hydration/samples/*/_after.html
packages/svelte/tests/hydration/samples/*/_after_head.html
packages/svelte/types packages/svelte/types
packages/svelte/compiler.cjs packages/svelte/compiler/index.js
playgrounds/demo/src playgrounds/demo/src
playgrounds/sandbox/input/**.svelte playgrounds/sandbox/input/**.svelte
playgrounds/sandbox/output playgrounds/sandbox/output

@ -37,7 +37,7 @@
"eslint": "^8.56.0", "eslint": "^8.56.0",
"eslint-plugin-lube": "^0.4.3", "eslint-plugin-lube": "^0.4.3",
"jsdom": "22.0.0", "jsdom": "22.0.0",
"playwright": "^1.35.1", "playwright": "^1.41.1",
"prettier": "^3.2.4", "prettier": "^3.2.4",
"prettier-plugin-svelte": "^3.1.2", "prettier-plugin-svelte": "^3.1.2",
"typescript": "^5.3.3", "typescript": "^5.3.3",

@ -1,6 +1,6 @@
/types/*.map /types/*.map
/types/compiler /types/compiler
/compiler.cjs /compiler/index.js
/action.d.ts /action.d.ts
/animate.d.ts /animate.d.ts

@ -1,5 +1,125 @@
# svelte # svelte
## 5.0.0-next.93
### Patch Changes
- breaking: prevent unparenthesized sequence expressions in attributes ([#11032](https://github.com/sveltejs/svelte/pull/11032))
- fix: ensure transition errors are not swallowed ([#11039](https://github.com/sveltejs/svelte/pull/11039))
## 5.0.0-next.92
### Patch Changes
- fix: include compiler/package.json in package ([#11033](https://github.com/sveltejs/svelte/pull/11033))
## 5.0.0-next.91
### Patch Changes
- fix: improve unowned derived signal heuristics ([#11029](https://github.com/sveltejs/svelte/pull/11029))
- fix: ensure correct context for action update/destroy functions ([#11023](https://github.com/sveltejs/svelte/pull/11023))
- feat: more efficient hydration markers ([#11019](https://github.com/sveltejs/svelte/pull/11019))
- fix: ensure effect cleanup functions are called with null `this` ([#11024](https://github.com/sveltejs/svelte/pull/11024))
- fix: correctly handle closure passed to $derived.by when destructuring ([#11028](https://github.com/sveltejs/svelte/pull/11028))
- Add `name` to HTMLDetailsAttributes ([#11013](https://github.com/sveltejs/svelte/pull/11013))
- breaking: move compiler.cjs to compiler/index.js ([#10988](https://github.com/sveltejs/svelte/pull/10988))
## 5.0.0-next.90
### Patch Changes
- fix: hydrate HTML with surrounding whitespace ([#10996](https://github.com/sveltejs/svelte/pull/10996))
- feat: faster HTML tags ([#10986](https://github.com/sveltejs/svelte/pull/10986))
## 5.0.0-next.89
### Patch Changes
- fix: expose 'svelte/internal' to prevent Vite erroring on startup ([#10987](https://github.com/sveltejs/svelte/pull/10987))
- fix: revert SSR shorthand comments ([#10980](https://github.com/sveltejs/svelte/pull/10980))
- fix: child effects are removed from parent branches ([#10985](https://github.com/sveltejs/svelte/pull/10985))
## 5.0.0-next.88
### Patch Changes
- fix: further improvements to effect scheduling and flushing ([#10971](https://github.com/sveltejs/svelte/pull/10971))
- feat: re-export built-ins from `svelte/reactivity` on the server ([#10973](https://github.com/sveltejs/svelte/pull/10973))
## 5.0.0-next.87
### Patch Changes
- fix: apply animate on prefix/suffix each block mutations ([#10965](https://github.com/sveltejs/svelte/pull/10965))
## 5.0.0-next.86
### Patch Changes
- fix: improved effect sequencing and execution order ([#10949](https://github.com/sveltejs/svelte/pull/10949))
- breaking: onDestroy functions run child-first ([#10949](https://github.com/sveltejs/svelte/pull/10949))
- fix: improve action support for nested $effect ([#10962](https://github.com/sveltejs/svelte/pull/10962))
## 5.0.0-next.85
### Patch Changes
- feat: use implicit return for each block keys ([#10938](https://github.com/sveltejs/svelte/pull/10938))
- breaking: always run pre effects immediately ([#10928](https://github.com/sveltejs/svelte/pull/10928))
- fix: improve order of pre-effect execution ([#10942](https://github.com/sveltejs/svelte/pull/10942))
- feat: more efficient each block compiler output ([#10937](https://github.com/sveltejs/svelte/pull/10937))
## 5.0.0-next.84
### Patch Changes
- fix: reliably remove undefined attributes during hydration ([#10917](https://github.com/sveltejs/svelte/pull/10917))
- fix: Add `elementtiming` HTMLAttribute, remove `crossorigin` from HTMLInputAttributes ([#10921](https://github.com/sveltejs/svelte/pull/10921))
- feat: shorter compiler output for attribute updates ([#10917](https://github.com/sveltejs/svelte/pull/10917))
## 5.0.0-next.83
### Patch Changes
- feat: more efficient if block compiler output ([#10906](https://github.com/sveltejs/svelte/pull/10906))
- fix: update type of `options.target` ([#10892](https://github.com/sveltejs/svelte/pull/10892))
- fix: correctly hydrate controlled each-else block ([#10887](https://github.com/sveltejs/svelte/pull/10887))
- fix: Add `dirname` to HTMLInputAttributes ([#10908](https://github.com/sveltejs/svelte/pull/10908))
## 5.0.0-next.82
### Patch Changes
- fix: allow runes for variable declarations in the template ([#10879](https://github.com/sveltejs/svelte/pull/10879))
- feat: take form resets into account for two way bindings ([#10617](https://github.com/sveltejs/svelte/pull/10617))
- fix: handle multiple snippet parameters with one or more being optional ([#10833](https://github.com/sveltejs/svelte/pull/10833))
- breaking: apply fallback value every time in runes mode ([#10797](https://github.com/sveltejs/svelte/pull/10797))
## 5.0.0-next.81 ## 5.0.0-next.81
### Patch Changes ### Patch Changes

@ -0,0 +1,3 @@
{
"type": "commonjs"
}

@ -712,6 +712,7 @@ export interface HTMLAttributes<T extends EventTarget> extends AriaAttributes, D
contextmenu?: string | undefined | null; contextmenu?: string | undefined | null;
dir?: string | undefined | null; dir?: string | undefined | null;
draggable?: Booleanish | undefined | null; draggable?: Booleanish | undefined | null;
elementtiming?: string | undefined | null;
enterkeyhint?: enterkeyhint?:
| 'enter' | 'enter'
| 'done' | 'done'
@ -905,6 +906,7 @@ export interface HTMLDataAttributes extends HTMLAttributes<HTMLDataElement> {
export interface HTMLDetailsAttributes extends HTMLAttributes<HTMLDetailsElement> { export interface HTMLDetailsAttributes extends HTMLAttributes<HTMLDetailsElement> {
open?: boolean | undefined | null; open?: boolean | undefined | null;
name?: string | undefined | null;
'bind:open'?: boolean | undefined | null; 'bind:open'?: boolean | undefined | null;
@ -1029,7 +1031,7 @@ export interface HTMLInputAttributes extends HTMLAttributes<HTMLInputElement> {
autocomplete?: string | undefined | null; autocomplete?: string | undefined | null;
capture?: boolean | 'user' | 'environment' | undefined | null; // https://www.w3.org/TR/html-media-capture/#the-capture-attribute capture?: boolean | 'user' | 'environment' | undefined | null; // https://www.w3.org/TR/html-media-capture/#the-capture-attribute
checked?: boolean | undefined | null; checked?: boolean | undefined | null;
crossorigin?: string | undefined | null; dirname?: string | undefined | null;
disabled?: boolean | undefined | null; disabled?: boolean | undefined | null;
form?: string | undefined | null; form?: string | undefined | null;
formaction?: string | undefined | null; formaction?: string | undefined | null;

@ -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.81", "version": "5.0.0-next.93",
"type": "module", "type": "module",
"types": "./types/index.d.ts", "types": "./types/index.d.ts",
"engines": { "engines": {
@ -12,17 +12,17 @@
"src", "src",
"!src/**/*.test.*", "!src/**/*.test.*",
"types", "types",
"compiler.cjs", "compiler",
"*.d.ts", "*.d.ts",
"README.md" "README.md"
], ],
"module": "src/main/main-client.js", "module": "src/index-client.js",
"main": "src/main/main-client.js", "main": "src/index-client.js",
"exports": { "exports": {
".": { ".": {
"types": "./types/index.d.ts", "types": "./types/index.d.ts",
"browser": "./src/main/main-client.js", "browser": "./src/index-client.js",
"default": "./src/main/main-server.js" "default": "./src/index-server.js"
}, },
"./package.json": "./package.json", "./package.json": "./package.json",
"./action": { "./action": {
@ -34,7 +34,7 @@
}, },
"./compiler": { "./compiler": {
"types": "./types/index.d.ts", "types": "./types/index.d.ts",
"require": "./compiler.cjs", "require": "./compiler/index.js",
"default": "./src/compiler/index.js" "default": "./src/compiler/index.js"
}, },
"./easing": { "./easing": {
@ -47,6 +47,9 @@
"./internal": { "./internal": {
"default": "./src/internal/index.js" "default": "./src/internal/index.js"
}, },
"./internal/client": {
"default": "./src/internal/client/index.js"
},
"./internal/disclose-version": { "./internal/disclose-version": {
"default": "./src/internal/disclose-version.js" "default": "./src/internal/disclose-version.js"
}, },
@ -64,7 +67,8 @@
}, },
"./reactivity": { "./reactivity": {
"types": "./types/index.d.ts", "types": "./types/index.d.ts",
"default": "./src/reactivity/index.js" "browser": "./src/reactivity/index-client.js",
"default": "./src/reactivity/index-server.js"
}, },
"./server": { "./server": {
"types": "./types/index.d.ts", "types": "./types/index.d.ts",
@ -137,7 +141,9 @@
"knip": { "knip": {
"entry": [ "entry": [
"src/*/index.js", "src/*/index.js",
"src/*/public.d.ts" "src/index-client.ts",
"src/index-server.ts",
"src/index.d.ts"
], ],
"project": [ "project": [
"src/**" "src/**"

@ -9,7 +9,7 @@ import './scripts/generate-version.js';
export default defineConfig({ export default defineConfig({
input: 'src/compiler/index.js', input: 'src/compiler/index.js',
output: { output: {
file: 'compiler.cjs', file: 'compiler/index.js',
format: 'umd', format: 'umd',
name: 'svelte' name: 'svelte'
}, },

@ -51,6 +51,7 @@ console.group('checking treeshakeability');
for (const key in pkg.exports) { for (const key in pkg.exports) {
// special cases // special cases
if (key === './compiler') continue; if (key === './compiler') continue;
if (key === './internal') continue;
if (key === './internal/disclose-version') continue; if (key === './internal/disclose-version') continue;
for (const type of ['browser', 'default']) { for (const type of ['browser', 'default']) {
@ -109,7 +110,7 @@ const bundle = await bundle_code(
).js.code ).js.code
); );
if (!bundle.includes('current_hydration_fragment')) { if (!bundle.includes('hydrate_nodes') && !bundle.includes('hydrate_anchor')) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.error(`✅ Hydration code treeshakeable`); console.error(`✅ Hydration code treeshakeable`);
} else { } else {

@ -22,14 +22,14 @@ fs.writeFileSync(`${dir}/types/compiler/interfaces.d.ts`, "import '../index.js';
await createBundle({ await createBundle({
output: `${dir}/types/index.d.ts`, output: `${dir}/types/index.d.ts`,
modules: { modules: {
[pkg.name]: `${dir}/src/main/public.d.ts`, [pkg.name]: `${dir}/src/index.d.ts`,
[`${pkg.name}/action`]: `${dir}/src/action/public.d.ts`, [`${pkg.name}/action`]: `${dir}/src/action/public.d.ts`,
[`${pkg.name}/animate`]: `${dir}/src/animate/public.d.ts`, [`${pkg.name}/animate`]: `${dir}/src/animate/public.d.ts`,
[`${pkg.name}/compiler`]: `${dir}/src/compiler/index.js`, [`${pkg.name}/compiler`]: `${dir}/src/compiler/index.js`,
[`${pkg.name}/easing`]: `${dir}/src/easing/index.js`, [`${pkg.name}/easing`]: `${dir}/src/easing/index.js`,
[`${pkg.name}/legacy`]: `${dir}/src/legacy/legacy-client.js`, [`${pkg.name}/legacy`]: `${dir}/src/legacy/legacy-client.js`,
[`${pkg.name}/motion`]: `${dir}/src/motion/public.d.ts`, [`${pkg.name}/motion`]: `${dir}/src/motion/public.d.ts`,
[`${pkg.name}/reactivity`]: `${dir}/src/reactivity/index.js`, [`${pkg.name}/reactivity`]: `${dir}/src/reactivity/index-client.js`,
[`${pkg.name}/server`]: `${dir}/src/server/index.js`, [`${pkg.name}/server`]: `${dir}/src/server/index.js`,
[`${pkg.name}/store`]: `${dir}/src/store/public.d.ts`, [`${pkg.name}/store`]: `${dir}/src/store/public.d.ts`,
[`${pkg.name}/transition`]: `${dir}/src/transition/public.d.ts`, [`${pkg.name}/transition`]: `${dir}/src/transition/public.d.ts`,

@ -172,13 +172,24 @@ declare namespace $effect {
* Declares the props that a component accepts. Example: * Declares the props that a component accepts. Example:
* *
* ```ts * ```ts
* let { optionalProp = 42, requiredProp }: { optionalProp?: number; requiredProps: string } = $props(); * let { optionalProp = 42, requiredProp, bindableProp = $bindable() }: { optionalProp?: number; requiredProps: string; bindableProp: boolean } = $props();
* ``` * ```
* *
* https://svelte-5-preview.vercel.app/docs/runes#$props * https://svelte-5-preview.vercel.app/docs/runes#$props
*/ */
declare function $props(): any; declare function $props(): any;
/**
* Declares a prop as bindable, meaning the parent component can use `bind:propName={value}` to bind to it.
*
* ```ts
* let { propName = $bindable() }: { propName: boolean } = $props();
* ```
*
* https://svelte-5-preview.vercel.app/docs/runes#$bindable
*/
declare function $bindable<T>(t?: T): T;
/** /**
* Inspects one or more values whenever they, or the properties they contain, change. Example: * Inspects one or more values whenever they, or the properties they contain, change. Example:
* *

@ -182,6 +182,7 @@ const runes = {
`$props() assignment must not contain nested properties or computed keys`, `$props() assignment must not contain nested properties or computed keys`,
'invalid-props-location': () => 'invalid-props-location': () =>
`$props() can only be used at the top level of components as a variable declaration initializer`, `$props() can only be used at the top level of components as a variable declaration initializer`,
'invalid-bindable-location': () => `$bindable() can only be used inside a $props() declaration`,
/** @param {string} rune */ /** @param {string} rune */
'invalid-state-location': (rune) => 'invalid-state-location': (rune) =>
`${rune}(...) can only be used as a variable declaration initializer or a class field`, `${rune}(...) can only be used as a variable declaration initializer or a class field`,
@ -281,7 +282,9 @@ const attributes = {
}, },
'invalid-let-directive-placement': () => 'let directive at invalid position', 'invalid-let-directive-placement': () => 'let directive at invalid position',
'invalid-style-directive-modifier': () => 'invalid-style-directive-modifier': () =>
`Invalid 'style:' modifier. Valid modifiers are: 'important'` `Invalid 'style:' modifier. Valid modifiers are: 'important'`,
'invalid-sequence-expression': () =>
`Sequence expressions are not allowed as attribute/directive values in runes mode, unless wrapped in parentheses`
}; };
/** @satisfies {Errors} */ /** @satisfies {Errors} */

@ -41,7 +41,7 @@ export function compile(source, options) {
}; };
} }
const analysis = analyze_component(parsed, combined_options); const analysis = analyze_component(parsed, source, combined_options);
const result = transform_component(analysis, source, combined_options); const result = transform_component(analysis, source, combined_options);
return result; return result;

@ -121,7 +121,10 @@ function read_type_annotation(parser, optional_allowed = false) {
const template = const template =
parser.template.slice(0, a).replace(/[^\n]/g, ' ') + parser.template.slice(0, a).replace(/[^\n]/g, ' ') +
insert + insert +
parser.template.slice(parser.index); // If this is a type annotation for a function parameter, Acorn-TS will treat subsequent
// parameters as part of a sequence expression instead, and will then error on optional
// parameters (`?:`). Therefore replace that sequence with something that will not error.
parser.template.slice(parser.index).replace(/\?\s*:/g, ':');
let expression = parse_expression_at(template, parser.ts, a); let expression = parse_expression_at(template, parser.ts, a);
// `foo: bar = baz` gets mangled — fix it // `foo: bar = baz` gets mangled — fix it

@ -258,10 +258,11 @@ export function analyze_module(ast, options) {
/** /**
* @param {import('#compiler').Root} root * @param {import('#compiler').Root} root
* @param {string} source
* @param {import('#compiler').ValidatedCompileOptions} options * @param {import('#compiler').ValidatedCompileOptions} options
* @returns {import('../types.js').ComponentAnalysis} * @returns {import('../types.js').ComponentAnalysis}
*/ */
export function analyze_component(root, options) { export function analyze_component(root, source, options) {
const scope_root = new ScopeRoot(); const scope_root = new ScopeRoot();
const module = js(root.module, scope_root, false, null); const module = js(root.module, scope_root, false, null);
@ -396,7 +397,8 @@ export function analyze_component(root, options) {
}) })
: '', : '',
keyframes: [] keyframes: []
} },
source
}; };
if (!options.customElement && root.options?.customElement) { if (!options.customElement && root.options?.customElement) {
@ -436,7 +438,7 @@ export function analyze_component(root, options) {
); );
} }
} else { } else {
instance.scope.declare(b.id('$$props'), 'prop', 'synthetic'); instance.scope.declare(b.id('$$props'), 'bindable_prop', 'synthetic');
instance.scope.declare(b.id('$$restProps'), 'rest_prop', 'synthetic'); instance.scope.declare(b.id('$$restProps'), 'rest_prop', 'synthetic');
for (const { ast, scope, scopes } of [module, instance, template]) { for (const { ast, scope, scopes } of [module, instance, template]) {
@ -466,7 +468,10 @@ export function analyze_component(root, options) {
} }
for (const [name, binding] of instance.scope.declarations) { for (const [name, binding] of instance.scope.declarations) {
if (binding.kind === 'prop' && binding.node.name !== '$$props') { if (
(binding.kind === 'prop' || binding.kind === 'bindable_prop') &&
binding.node.name !== '$$props'
) {
const references = binding.references.filter( const references = binding.references.filter(
(r) => r.node !== binding.node && r.path.at(-1)?.type !== 'ExportSpecifier' (r) => r.node !== binding.node && r.path.at(-1)?.type !== 'ExportSpecifier'
); );
@ -605,20 +610,38 @@ const legacy_scope_tweaker = {
/** @type {import('../types.js').ReactiveStatement} */ /** @type {import('../types.js').ReactiveStatement} */
const reactive_statement = { const reactive_statement = {
assignments: new Set(), assignments: new Set(),
dependencies: new Set() dependencies: []
}; };
next({ ...state, reactive_statement, function_depth: state.scope.function_depth + 1 }); next({ ...state, reactive_statement, function_depth: state.scope.function_depth + 1 });
// Every referenced binding becomes a dependency, unless it's on
// the left-hand side of an `=` assignment
for (const [name, nodes] of state.scope.references) { for (const [name, nodes] of state.scope.references) {
const binding = state.scope.get(name); const binding = state.scope.get(name);
if (binding === null) continue; if (binding === null) continue;
// Include bindings that have references other than assignments and their own declarations for (const { node, path } of nodes) {
/** @type {import('estree').Expression} */
let left = node;
let i = path.length - 1;
let parent = /** @type {import('estree').Expression} */ (path.at(i));
while (parent.type === 'MemberExpression') {
left = parent;
parent = /** @type {import('estree').Expression} */ (path.at(--i));
}
if ( if (
nodes.some((n) => n.node !== binding.node && !reactive_statement.assignments.has(n.node)) parent.type === 'AssignmentExpression' &&
parent.operator === '=' &&
parent.left === left
) { ) {
reactive_statement.dependencies.add(binding); continue;
}
reactive_statement.dependencies.push(binding);
break;
} }
} }
@ -627,8 +650,8 @@ const legacy_scope_tweaker = {
// Ideally this would be in the validation file, but that isn't possible because this visitor // Ideally this would be in the validation file, but that isn't possible because this visitor
// calls "next" before setting the reactive statements. // calls "next" before setting the reactive statements.
if ( if (
reactive_statement.dependencies.size && reactive_statement.dependencies.length &&
[...reactive_statement.dependencies].every( reactive_statement.dependencies.every(
(d) => d.scope === state.analysis.module.scope && d.declaration_kind !== 'const' (d) => d.scope === state.analysis.module.scope && d.declaration_kind !== 'const'
) )
) { ) {
@ -657,15 +680,29 @@ const legacy_scope_tweaker = {
} }
}, },
AssignmentExpression(node, { state, next }) { AssignmentExpression(node, { state, next }) {
if (state.reactive_statement && node.operator === '=') { if (state.reactive_statement) {
if (node.left.type === 'MemberExpression') { const id = node.left.type === 'MemberExpression' ? object(node.left) : node.left;
const id = object(node.left);
if (id !== null) { if (id !== null) {
state.reactive_statement.assignments.add(id);
}
} else {
for (const id of extract_identifiers(node.left)) { for (const id of extract_identifiers(node.left)) {
state.reactive_statement.assignments.add(id); const binding = state.scope.get(id.name);
if (binding) {
state.reactive_statement.assignments.add(binding);
}
}
}
}
next();
},
UpdateExpression(node, { state, next }) {
if (state.reactive_statement) {
const id = node.argument.type === 'MemberExpression' ? object(node.argument) : node.argument;
if (id?.type === 'Identifier') {
const binding = state.scope.get(id.name);
if (binding) {
state.reactive_statement.assignments.add(binding);
} }
} }
} }
@ -753,12 +790,13 @@ const legacy_scope_tweaker = {
state.scope.get(specifier.local.name) state.scope.get(specifier.local.name)
); );
if ( if (
binding.kind === 'state' || binding !== null &&
(binding.kind === 'state' ||
binding.kind === 'frozen_state' || binding.kind === 'frozen_state' ||
(binding.kind === 'normal' && (binding.kind === 'normal' &&
(binding.declaration_kind === 'let' || binding.declaration_kind === 'var')) (binding.declaration_kind === 'let' || binding.declaration_kind === 'var')))
) { ) {
binding.kind = 'prop'; binding.kind = 'bindable_prop';
if (specifier.exported.name !== specifier.local.name) { if (specifier.exported.name !== specifier.local.name) {
binding.prop_alias = specifier.exported.name; binding.prop_alias = specifier.exported.name;
} }
@ -796,7 +834,7 @@ const legacy_scope_tweaker = {
for (const declarator of node.declaration.declarations) { for (const declarator of node.declaration.declarations) {
for (const id of extract_identifiers(declarator.id)) { for (const id of extract_identifiers(declarator.id)) {
const binding = /** @type {import('#compiler').Binding} */ (state.scope.get(id.name)); const binding = /** @type {import('#compiler').Binding} */ (state.scope.get(id.name));
binding.kind = 'prop'; binding.kind = 'bindable_prop';
} }
} }
} }
@ -885,11 +923,24 @@ const runes_scope_tweaker = {
property.key.type === 'Identifier' property.key.type === 'Identifier'
? property.key.name ? property.key.name
: /** @type {string} */ (/** @type {import('estree').Literal} */ (property.key).value); : /** @type {string} */ (/** @type {import('estree').Literal} */ (property.key).value);
const initial = property.value.type === 'AssignmentPattern' ? property.value.right : null; let initial = property.value.type === 'AssignmentPattern' ? property.value.right : null;
const binding = /** @type {import('#compiler').Binding} */ (state.scope.get(name)); const binding = /** @type {import('#compiler').Binding} */ (state.scope.get(name));
binding.prop_alias = alias; binding.prop_alias = alias;
binding.initial = initial; // rewire initial from $props() to the actual initial value
// rewire initial from $props() to the actual initial value, stripping $bindable() if necessary
if (
initial?.type === 'CallExpression' &&
initial.callee.type === 'Identifier' &&
initial.callee.name === '$bindable'
) {
binding.initial = /** @type {import('estree').Expression | null} */ (
initial.arguments[0] ?? null
);
binding.kind = 'bindable_prop';
} else {
binding.initial = initial;
}
} }
} }
}, },
@ -935,25 +986,6 @@ const runes_scope_tweaker = {
function is_known_safe_call(node, context) { function is_known_safe_call(node, context) {
const callee = node.callee; const callee = node.callee;
// Check for selector() API calls
if (callee.type === 'MemberExpression' && callee.object.type === 'Identifier') {
const binding = context.state.scope.get(callee.object.name);
const selector_binding = context.state.scope.get('selector');
if (
selector_binding !== null &&
selector_binding.declaration_kind === 'import' &&
selector_binding.initial !== null &&
selector_binding.initial.type === 'ImportDeclaration' &&
selector_binding.initial.source.value === 'svelte' &&
binding !== null &&
binding.initial !== null &&
binding.initial.type === 'CallExpression' &&
binding.initial.callee.type === 'Identifier' &&
binding.initial.callee.name === 'selector'
) {
return true;
}
}
// String / Number / BigInt / Boolean casting calls // String / Number / BigInt / Boolean casting calls
if (callee.type === 'Identifier') { if (callee.type === 'Identifier') {
const name = callee.name; const name = callee.name;
@ -1345,21 +1377,21 @@ function order_reactive_statements(unsorted_reactive_declarations) {
const lookup = new Map(); const lookup = new Map();
for (const [node, declaration] of unsorted_reactive_declarations) { for (const [node, declaration] of unsorted_reactive_declarations) {
declaration.assignments.forEach(({ name }) => { for (const binding of declaration.assignments) {
const statements = lookup.get(name) ?? []; const statements = lookup.get(binding.node.name) ?? [];
statements.push([node, declaration]); statements.push([node, declaration]);
lookup.set(name, statements); lookup.set(binding.node.name, statements);
}); }
} }
/** @type {Array<[string, string]>} */ /** @type {Array<[string, string]>} */
const edges = []; const edges = [];
for (const [, { assignments, dependencies }] of unsorted_reactive_declarations) { for (const [, { assignments, dependencies }] of unsorted_reactive_declarations) {
for (const { name } of assignments) { for (const assignment of assignments) {
for (const { node } of dependencies) { for (const dependency of dependencies) {
if (![...assignments].find(({ name }) => node.name === name)) { if (!assignments.has(dependency)) {
edges.push([name, node.name]); edges.push([assignment.node.name, dependency.node.name]);
} }
} }
} }
@ -1383,14 +1415,17 @@ function order_reactive_statements(unsorted_reactive_declarations) {
*/ */
const add_declaration = (node, declaration) => { const add_declaration = (node, declaration) => {
if ([...reactive_declarations.values()].includes(declaration)) return; if ([...reactive_declarations.values()].includes(declaration)) return;
declaration.dependencies.forEach(({ node: { name } }) => {
if ([...declaration.assignments].some((a) => a.name === name)) return; for (const binding of declaration.dependencies) {
for (const [node, earlier] of lookup.get(name) ?? []) { if (declaration.assignments.has(binding)) continue;
for (const [node, earlier] of lookup.get(binding.node.name) ?? []) {
add_declaration(node, earlier); add_declaration(node, earlier);
} }
}); }
reactive_declarations.set(node, declaration); reactive_declarations.set(node, declaration);
}; };
for (const [node, declaration] of unsorted_reactive_declarations) { for (const [node, declaration] of unsorted_reactive_declarations) {
add_declaration(node, declaration); add_declaration(node, declaration);
} }

@ -1,13 +0,0 @@
/**
* Pushes all `items` into `array` using `push`, therefore mutating the array.
* We do this for memory and perf reasons, and because `array.push(...items)` would
* run into a "max call stack size exceeded" error with too many items (~65k).
* @template T
* @param {T[]} array
* @param {T[]} items
*/
export function push_array(array, items) {
for (let i = 0; i < items.length; i++) {
array.push(items[i]);
}
}

@ -50,6 +50,18 @@ function validate_component(node, context) {
} }
if (attribute.type === 'Attribute') { if (attribute.type === 'Attribute') {
if (context.state.analysis.runes && is_expression_attribute(attribute)) {
const expression = attribute.value[0].expression;
if (expression.type === 'SequenceExpression') {
let i = /** @type {number} */ (expression.start);
while (--i > 0) {
const char = context.state.analysis.source[i];
if (char === '(') break; // parenthesized sequence expressions are ok
if (char === '{') error(expression, 'invalid-sequence-expression');
}
}
}
validate_attribute_name(attribute, context); validate_attribute_name(attribute, context);
if (attribute.name === 'slot') { if (attribute.name === 'slot') {
@ -81,12 +93,21 @@ function validate_element(node, context) {
for (const attribute of node.attributes) { for (const attribute of node.attributes) {
if (attribute.type === 'Attribute') { if (attribute.type === 'Attribute') {
const is_expression = is_expression_attribute(attribute);
if (context.state.analysis.runes && is_expression) {
const expression = attribute.value[0].expression;
if (expression.type === 'SequenceExpression') {
error(expression, 'invalid-sequence-expression');
}
}
if (regex_illegal_attribute_character.test(attribute.name)) { if (regex_illegal_attribute_character.test(attribute.name)) {
error(attribute, 'invalid-attribute-name', attribute.name); error(attribute, 'invalid-attribute-name', attribute.name);
} }
if (attribute.name.startsWith('on') && attribute.name.length > 2) { if (attribute.name.startsWith('on') && attribute.name.length > 2) {
if (!is_expression_attribute(attribute)) { if (!is_expression) {
error(attribute, 'invalid-event-attribute-value'); error(attribute, 'invalid-event-attribute-value');
} }
@ -299,17 +320,19 @@ const validation = {
error(node, 'invalid-binding-expression'); error(node, 'invalid-binding-expression');
} }
const binding = context.state.scope.get(left.name);
if ( if (
assignee.type === 'Identifier' && assignee.type === 'Identifier' &&
node.name !== 'this' // bind:this also works for regular variables node.name !== 'this' // bind:this also works for regular variables
) { ) {
const binding = context.state.scope.get(left.name);
// reassignment // reassignment
if ( if (
!binding || !binding ||
(binding.kind !== 'state' && (binding.kind !== 'state' &&
binding.kind !== 'frozen_state' && binding.kind !== 'frozen_state' &&
binding.kind !== 'prop' && binding.kind !== 'prop' &&
binding.kind !== 'bindable_prop' &&
binding.kind !== 'each' && binding.kind !== 'each' &&
binding.kind !== 'store_sub' && binding.kind !== 'store_sub' &&
!binding.mutated) !binding.mutated)
@ -328,8 +351,6 @@ 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') {
if (!binding) { if (!binding) {
error(node, 'INTERNAL', 'Cannot find declaration for bind:group'); error(node, 'INTERNAL', 'Cannot find declaration for bind:group');
@ -780,7 +801,25 @@ function validate_call_expression(node, scope, path) {
error(node, 'invalid-props-location'); error(node, 'invalid-props-location');
} }
if (rune === '$state' || rune === '$derived' || rune === '$derived.by') { if (rune === '$bindable') {
if (parent.type === 'AssignmentPattern' && path.at(-3)?.type === 'ObjectPattern') {
const declarator = path.at(-4);
if (
declarator?.type === 'VariableDeclarator' &&
get_rune(declarator.init, scope) === '$props'
) {
return;
}
}
error(node, 'invalid-bindable-location');
}
if (
rune === '$state' ||
rune === '$state.frozen' ||
rune === '$derived' ||
rune === '$derived.by'
) {
if (parent.type === 'VariableDeclarator') return; if (parent.type === 'VariableDeclarator') return;
if (parent.type === 'PropertyDefinition' && !parent.static && !parent.computed) return; if (parent.type === 'PropertyDefinition' && !parent.static && !parent.computed) return;
error(node, 'invalid-state-location', rune); error(node, 'invalid-state-location', rune);
@ -873,6 +912,8 @@ export const validation_runes_js = {
error(node, 'invalid-rune-args-length', rune, [0, 1]); error(node, 'invalid-rune-args-length', rune, [0, 1]);
} else if (rune === '$props') { } else if (rune === '$props') {
error(node, 'invalid-props-location'); error(node, 'invalid-props-location');
} else if (rune === '$bindable') {
error(node, 'invalid-bindable-location');
} }
}, },
AssignmentExpression(node, { state }) { AssignmentExpression(node, { state }) {
@ -1022,6 +1063,9 @@ export const validation_runes = merge(validation, a11y_validators, {
} }
}, },
CallExpression(node, { state, path }) { CallExpression(node, { state, path }) {
if (get_rune(node, state.scope) === '$bindable' && node.arguments.length > 1) {
error(node, 'invalid-rune-args-length', '$bindable', [0, 1]);
}
validate_call_expression(node, state.scope, path); validate_call_expression(node, state.scope, path);
}, },
EachBlock(node, { next, state }) { EachBlock(node, { next, state }) {
@ -1062,7 +1106,7 @@ export const validation_runes = merge(validation, a11y_validators, {
state.has_props_rune = true; state.has_props_rune = true;
if (args.length > 0) { if (args.length > 0) {
error(node, 'invalid-rune-args-length', '$props', [0]); error(node, 'invalid-rune-args-length', rune, [0]);
} }
if (node.id.type !== 'ObjectPattern') { if (node.id.type !== 'ObjectPattern') {

@ -46,27 +46,27 @@ export function client_component(source, analysis, options) {
options, options,
scope: analysis.module.scope, scope: analysis.module.scope,
scopes: analysis.template.scopes, scopes: analysis.template.scopes,
hoisted: [b.import_all('$', 'svelte/internal')], hoisted: [b.import_all('$', 'svelte/internal/client')],
node: /** @type {any} */ (null), // populated by the root node node: /** @type {any} */ (null), // populated by the root node
// these should be set by create_block - if they're called outside, it's a bug // these should be set by create_block - if they're called outside, it's a bug
get init() { get before_init() {
/** @type {any[]} */ /** @type {any[]} */
const a = []; const a = [];
a.push = () => error(null, 'INTERNAL', 'init.push should not be called outside create_block'); a.push = () =>
error(null, 'INTERNAL', 'before_init.push should not be called outside create_block');
return a; return a;
}, },
get update() { get init() {
/** @type {any[]} */ /** @type {any[]} */
const a = []; const a = [];
a.push = () => a.push = () => error(null, 'INTERNAL', 'init.push should not be called outside create_block');
error(null, 'INTERNAL', 'update.push should not be called outside create_block');
return a; return a;
}, },
get update_effects() { get update() {
/** @type {any[]} */ /** @type {any[]} */
const a = []; const a = [];
a.push = () => a.push = () =>
error(null, 'INTERNAL', 'update_effects.push should not be called outside create_block'); error(null, 'INTERNAL', 'update.push should not be called outside create_block');
return a; return a;
}, },
get after_update() { get after_update() {
@ -239,7 +239,7 @@ export function client_component(source, analysis, options) {
); );
}); });
const properties = analysis.exports.map(({ name, alias }) => { const component_returned_object = analysis.exports.map(({ name, alias }) => {
const expression = serialize_get_binding(b.id(name), instance_state); const expression = serialize_get_binding(b.id(name), instance_state);
if (expression.type === 'Identifier' && !options.dev) { if (expression.type === 'Identifier' && !options.dev) {
@ -249,17 +249,33 @@ export function client_component(source, analysis, options) {
return b.get(alias ?? name, [b.return(expression)]); return b.get(alias ?? name, [b.return(expression)]);
}); });
if (analysis.accessors) { const properties = [...analysis.instance.scope.declarations].filter(
for (const [name, binding] of analysis.instance.scope.declarations) { ([name, binding]) =>
if (binding.kind !== 'prop' || name.startsWith('$$')) continue; (binding.kind === 'prop' || binding.kind === 'bindable_prop') && !name.startsWith('$$')
);
if (analysis.runes && options.dev) {
/** @type {import('estree').Literal[]} */
const bindable = [];
for (const [name, binding] of properties) {
if (binding.kind === 'bindable_prop') {
bindable.push(b.literal(binding.prop_alias ?? name));
}
}
instance.body.unshift(
b.stmt(b.call('$.validate_prop_bindings', b.id('$$props'), b.array(bindable)))
);
}
if (analysis.accessors) {
for (const [name, binding] of properties) {
const key = binding.prop_alias ?? name; const key = binding.prop_alias ?? name;
const getter = b.get(key, [b.return(b.call(b.id(name)))]); const getter = b.get(key, [b.return(b.call(b.id(name)))]);
const setter = b.set(key, [ const setter = b.set(key, [
b.stmt(b.call(b.id(name), b.id('$$value'))), b.stmt(b.call(b.id(name), b.id('$$value'))),
b.stmt(b.call('$.flushSync')) b.stmt(b.call('$.flush_sync'))
]); ]);
if (analysis.runes && binding.initial) { if (analysis.runes && binding.initial) {
@ -271,12 +287,12 @@ export function client_component(source, analysis, options) {
}; };
} }
properties.push(getter, setter); component_returned_object.push(getter, setter);
} }
} }
if (options.legacy.componentApi) { if (options.legacy.componentApi) {
properties.push( component_returned_object.push(
b.init('$set', b.id('$.update_legacy_props')), b.init('$set', b.id('$.update_legacy_props')),
b.init( b.init(
'$on', '$on',
@ -292,7 +308,7 @@ export function client_component(source, analysis, options) {
) )
); );
} else if (options.dev) { } else if (options.dev) {
properties.push( component_returned_object.push(
b.init( b.init(
'$set', '$set',
b.thunk( b.thunk(
@ -360,8 +376,8 @@ export function client_component(source, analysis, options) {
append_styles(); append_styles();
component_block.body.push( component_block.body.push(
properties.length > 0 component_returned_object.length > 0
? b.return(b.call('$.pop', b.object(properties))) ? b.return(b.call('$.pop', b.object(component_returned_object)))
: b.stmt(b.call('$.pop')) : b.stmt(b.call('$.pop'))
); );
@ -369,7 +385,7 @@ export function client_component(source, analysis, options) {
/** @type {string[]} */ /** @type {string[]} */
const named_props = analysis.exports.map(({ name, alias }) => alias ?? name); const named_props = analysis.exports.map(({ name, alias }) => alias ?? name);
for (const [name, binding] of analysis.instance.scope.declarations) { for (const [name, binding] of analysis.instance.scope.declarations) {
if (binding.kind === 'prop') named_props.push(binding.prop_alias ?? name); if (binding.kind === 'bindable_prop') named_props.push(binding.prop_alias ?? name);
} }
component_block.body.unshift( component_block.body.unshift(
@ -476,9 +492,7 @@ export function client_component(source, analysis, options) {
/** @type {import('estree').Property[]} */ /** @type {import('estree').Property[]} */
const props_str = []; const props_str = [];
for (const [name, binding] of analysis.instance.scope.declarations) { for (const [name, binding] of properties) {
if (binding.kind !== 'prop' || name.startsWith('$$')) continue;
const key = binding.prop_alias ?? name; const key = binding.prop_alias ?? name;
const prop_def = typeof ce === 'boolean' ? {} : ce.props?.[key] || {}; const prop_def = typeof ce === 'boolean' ? {} : ce.props?.[key] || {};
if ( if (
@ -568,6 +582,6 @@ export function client_module(analysis, options) {
return { return {
type: 'Program', type: 'Program',
sourceType: 'module', sourceType: 'module',
body: [b.import_all('$', 'svelte/internal'), ...module.body] body: [b.import_all('$', 'svelte/internal/client'), ...module.body]
}; };
} }

@ -29,18 +29,12 @@ export interface ComponentClientTransformState extends ClientTransformState {
readonly hoisted: Array<Statement | ModuleDeclaration>; readonly hoisted: Array<Statement | ModuleDeclaration>;
readonly events: Set<string>; readonly events: Set<string>;
/** Stuff that happens before the render effect(s) */
readonly before_init: Statement[];
/** Stuff that happens before the render effect(s) */ /** Stuff that happens before the render effect(s) */
readonly init: Statement[]; readonly init: Statement[];
/** Stuff that happens inside separate render effects (due to call expressions) */
readonly update_effects: Statement[];
/** Stuff that happens inside the render effect */ /** Stuff that happens inside the render effect */
readonly update: { readonly update: Statement[];
init?: Statement;
/** If the update array only contains a single entry, this singular entry will be used, if present */
singular?: Statement;
/** Used if condition for singular prop is false (see comment above) */
grouped: Statement;
}[];
/** Stuff that happens after the render effect (control blocks, dynamic elements, bindings, actions, etc) */ /** Stuff that happens after the render effect (control blocks, dynamic elements, bindings, actions, etc) */
readonly after_update: Statement[]; readonly after_update: Statement[];
/** The HTML template string */ /** The HTML template string */

@ -78,7 +78,7 @@ export function serialize_get_binding(node, state) {
return typeof binding.expression === 'function' ? binding.expression(node) : binding.expression; return typeof binding.expression === 'function' ? binding.expression(node) : binding.expression;
} }
if (binding.kind === 'prop') { if (binding.kind === 'prop' || binding.kind === 'bindable_prop') {
if (binding.node.name === '$$props') { if (binding.node.name === '$$props') {
// Special case for $$props which only exists in the old world // Special case for $$props which only exists in the old world
// TODO this probably shouldn't have a 'prop' binding kind // TODO this probably shouldn't have a 'prop' binding kind
@ -377,6 +377,7 @@ export function serialize_set_binding(node, context, fallback, options) {
binding.kind !== 'state' && binding.kind !== 'state' &&
binding.kind !== 'frozen_state' && binding.kind !== 'frozen_state' &&
binding.kind !== 'prop' && binding.kind !== 'prop' &&
binding.kind !== 'bindable_prop' &&
binding.kind !== 'each' && binding.kind !== 'each' &&
binding.kind !== 'legacy_reactive' && binding.kind !== 'legacy_reactive' &&
!is_store !is_store
@ -389,7 +390,7 @@ export function serialize_set_binding(node, context, fallback, options) {
const serialize = () => { const serialize = () => {
if (left === node.left) { if (left === node.left) {
if (binding.kind === 'prop') { if (binding.kind === 'prop' || binding.kind === 'bindable_prop') {
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);
@ -467,7 +468,7 @@ export function serialize_set_binding(node, context, fallback, options) {
b.call('$.untrack', b.id('$' + left_name)) b.call('$.untrack', b.id('$' + left_name))
); );
} else if (!state.analysis.runes) { } else if (!state.analysis.runes) {
if (binding.kind === 'prop') { if (binding.kind === 'bindable_prop') {
return b.call( return b.call(
left, left,
b.sequence([ b.sequence([
@ -571,7 +572,7 @@ function get_hoistable_params(node, context) {
params.push(b.id(binding.expression.object.arguments[0].name)); params.push(b.id(binding.expression.object.arguments[0].name));
} else if ( } else if (
// If we are referencing a simple $$props value, then we need to reference the object property instead // If we are referencing a simple $$props value, then we need to reference the object property instead
binding.kind === 'prop' && (binding.kind === 'prop' || binding.kind === 'bindable_prop') &&
!binding.reassigned && !binding.reassigned &&
binding.initial === null && binding.initial === null &&
!context.state.analysis.accessors !context.state.analysis.accessors

@ -52,6 +52,7 @@ export const global_visitors = {
binding?.kind === 'each' || binding?.kind === 'each' ||
binding?.kind === 'legacy_reactive' || binding?.kind === 'legacy_reactive' ||
binding?.kind === 'prop' || binding?.kind === 'prop' ||
binding?.kind === 'bindable_prop' ||
is_store is_store
) { ) {
/** @type {import('estree').Expression[]} */ /** @type {import('estree').Expression[]} */
@ -64,7 +65,7 @@ export const global_visitors = {
fn += '_store'; fn += '_store';
args.push(serialize_get_binding(b.id(name), state), b.call('$' + name)); args.push(serialize_get_binding(b.id(name), state), b.call('$' + name));
} else { } else {
if (binding.kind === 'prop') fn += '_prop'; if (binding.kind === 'prop' || binding.kind === 'bindable_prop') fn += '_prop';
args.push(b.id(name)); args.push(b.id(name));
} }

@ -40,7 +40,7 @@ export const javascript_visitors_legacy = {
state.scope.get_bindings(declarator) state.scope.get_bindings(declarator)
); );
const has_state = bindings.some((binding) => binding.kind === 'state'); const has_state = bindings.some((binding) => binding.kind === 'state');
const has_props = bindings.some((binding) => binding.kind === 'prop'); const has_props = bindings.some((binding) => binding.kind === 'bindable_prop');
if (!has_state && !has_props) { if (!has_state && !has_props) {
const init = declarator.init; const init = declarator.init;
@ -80,7 +80,7 @@ export const javascript_visitors_legacy = {
declarations.push( declarations.push(
b.declarator( b.declarator(
path.node, path.node,
binding.kind === 'prop' binding.kind === 'bindable_prop'
? get_prop_source(binding, state, binding.prop_alias ?? name, value) ? get_prop_source(binding, state, binding.prop_alias ?? name, value)
: value : value
) )
@ -168,7 +168,7 @@ 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 === 'bindable_prop') {
serialized = b.call('$.deep_read_state', serialized); serialized = b.call('$.deep_read_state', serialized);
} }

@ -207,34 +207,31 @@ export const javascript_visitors_runes = {
seen.push(name); seen.push(name);
let id = property.value; let id =
let initial = undefined; property.value.type === 'AssignmentPattern' ? property.value.left : property.value;
if (property.value.type === 'AssignmentPattern') {
id = property.value.left;
initial = /** @type {import('estree').Expression} */ (visit(property.value.right));
}
assert.equal(id.type, 'Identifier'); assert.equal(id.type, 'Identifier');
const binding = /** @type {import('#compiler').Binding} */ (state.scope.get(id.name)); const binding = /** @type {import('#compiler').Binding} */ (state.scope.get(id.name));
const initial =
binding.initial &&
/** @type {import('estree').Expression} */ (visit(binding.initial));
if (binding.reassigned || state.analysis.accessors || initial) { if (binding.reassigned || state.analysis.accessors || initial) {
declarations.push(b.declarator(id, get_prop_source(binding, state, name, initial))); declarations.push(b.declarator(id, get_prop_source(binding, state, name, initial)));
} }
} else { } else {
// RestElement // RestElement
declarations.push( /** @type {import('estree').Expression[]} */
b.declarator( const args = [b.id('$$props'), b.array(seen.map((name) => b.literal(name)))];
property.argument,
b.call( if (state.options.dev) {
'$.rest_props', // include rest name, so we can provide informative error messages
b.id('$$props'), args.push(
b.array(seen.map((name) => b.literal(name))) b.literal(/** @type {import('estree').Identifier} */ (property.argument).name)
)
)
); );
} }
declarations.push(b.declarator(property.argument, b.call('$.rest_props', ...args)));
}
} }
// TODO // TODO
@ -304,7 +301,7 @@ export const javascript_visitors_runes = {
declarations.push( declarations.push(
b.declarator( b.declarator(
b.id(object_id), b.id(object_id),
b.call('$.derived', b.thunk(rune === '$derived.by' ? b.call(value) : value)) b.call('$.derived', rune === '$derived.by' ? value : b.thunk(value))
) )
); );
declarations.push( declarations.push(
@ -374,7 +371,7 @@ export const javascript_visitors_runes = {
const func = context.visit(node.expression.arguments[0]); const func = context.visit(node.expression.arguments[0]);
return { return {
...node, ...node,
expression: b.call('$.pre_effect', /** @type {import('estree').Expression} */ (func)) expression: b.call('$.user_pre_effect', /** @type {import('estree').Expression} */ (func))
}; };
} }
} }
@ -392,7 +389,7 @@ export const javascript_visitors_runes = {
const args = /** @type {import('estree').Expression[]} */ ( const args = /** @type {import('estree').Expression[]} */ (
node.arguments.map((arg) => context.visit(arg)) node.arguments.map((arg) => context.visit(arg))
); );
return b.call('$.user_root_effect', ...args); return b.call('$.effect_root', ...args);
} }
if (rune === '$inspect' || rune === '$inspect().with') { if (rune === '$inspect' || rune === '$inspect().with') {

@ -24,8 +24,17 @@ import { create_attribute, is_custom_element_node, is_element_node } from '../..
import { error } from '../../../errors.js'; import { error } from '../../../errors.js';
import { binding_properties } from '../../bindings.js'; import { binding_properties } from '../../bindings.js';
import { regex_starts_with_newline, regex_whitespaces_strict } from '../../patterns.js'; import { regex_starts_with_newline, regex_whitespaces_strict } from '../../patterns.js';
import { DOMBooleanAttributes } from '../../../../constants.js'; import {
DOMBooleanAttributes,
HYDRATION_END,
HYDRATION_END_ELSE,
HYDRATION_START
} from '../../../../constants.js';
import { sanitize_template_string } from '../../../utils/sanitize_template_string.js'; import { sanitize_template_string } from '../../../utils/sanitize_template_string.js';
import { BLOCK_CLOSE, BLOCK_CLOSE_ELSE } from '../../../../internal/server/hydration.js';
export const block_open = t_string(`<!--${HYDRATION_START}-->`);
export const block_close = t_string(`<!--${HYDRATION_END}-->`);
/** /**
* @param {string} value * @param {string} value
@ -52,15 +61,6 @@ function t_statement(value) {
return { type: 'statement', value }; return { type: 'statement', value };
} }
/**
* @param {import('./types').ServerTransformState} state
* @returns {[import('estree').VariableDeclaration, import('estree').Identifier]}
*/
function serialize_anchor(state) {
const id = state.scope.root.unique('anchor');
return [b.const(id, b.call('$.create_anchor', b.id('$$payload'))), id];
}
/** /**
* @param {import('./types').Template[]} template * @param {import('./types').Template[]} template
* @param {import('estree').Identifier} out * @param {import('estree').Identifier} out
@ -446,6 +446,7 @@ function serialize_set_binding(node, context, fallback) {
binding.kind !== 'state' && binding.kind !== 'state' &&
binding.kind !== 'frozen_state' && binding.kind !== 'frozen_state' &&
binding.kind !== 'prop' && binding.kind !== 'prop' &&
binding.kind !== 'bindable_prop' &&
binding.kind !== 'each' && binding.kind !== 'each' &&
binding.kind !== 'legacy_reactive' && binding.kind !== 'legacy_reactive' &&
!is_store !is_store
@ -690,7 +691,21 @@ const javascript_visitors_runes = {
} }
if (rune === '$props') { if (rune === '$props') {
declarations.push(b.declarator(declarator.id, b.id('$$props'))); // remove $bindable() from props declaration
const id = walk(declarator.id, null, {
AssignmentPattern(node) {
if (
node.right.type === 'CallExpression' &&
get_rune(node.right, state.scope) === '$bindable'
) {
const right = node.right.arguments.length
? /** @type {import('estree').Expression} */ (visit(node.right.arguments[0]))
: b.id('undefined');
return b.assignment_pattern(node.left, right);
}
}
});
declarations.push(b.declarator(id, b.id('$$props')));
continue; continue;
} }
@ -1136,7 +1151,7 @@ const javascript_visitors_legacy = {
state.scope.get_bindings(declarator) state.scope.get_bindings(declarator)
); );
const has_state = bindings.some((binding) => binding.kind === 'state'); const has_state = bindings.some((binding) => binding.kind === 'state');
const has_props = bindings.some((binding) => binding.kind === 'prop'); const has_props = bindings.some((binding) => binding.kind === 'bindable_prop');
if (!has_state && !has_props) { if (!has_state && !has_props) {
declarations.push(/** @type {import('estree').VariableDeclarator} */ (visit(declarator))); declarations.push(/** @type {import('estree').VariableDeclarator} */ (visit(declarator)));
@ -1227,12 +1242,10 @@ const template_visitors = {
}, },
HtmlTag(node, context) { HtmlTag(node, context) {
const state = context.state; const state = context.state;
const [dec, id] = serialize_anchor(state); state.template.push(block_open);
state.init.push(dec);
state.template.push(t_expression(id));
const raw = /** @type {import('estree').Expression} */ (context.visit(node.expression)); const raw = /** @type {import('estree').Expression} */ (context.visit(node.expression));
context.state.template.push(t_expression(raw)); context.state.template.push(t_expression(raw));
state.template.push(t_expression(id)); state.template.push(block_close);
}, },
ConstTag(node, { state, visit }) { ConstTag(node, { state, visit }) {
const declaration = node.declaration.declarations[0]; const declaration = node.declaration.declarations[0];
@ -1263,10 +1276,8 @@ const template_visitors = {
}, },
RenderTag(node, context) { RenderTag(node, context) {
const state = context.state; const state = context.state;
const [anchor, anchor_id] = serialize_anchor(state);
state.init.push(anchor); state.template.push(block_open);
state.template.push(t_expression(anchor_id));
const callee = unwrap_optional(node.expression).callee; const callee = unwrap_optional(node.expression).callee;
const raw_args = unwrap_optional(node.expression).arguments; const raw_args = unwrap_optional(node.expression).arguments;
@ -1292,7 +1303,7 @@ const template_visitors = {
) )
); );
state.template.push(t_expression(anchor_id)); state.template.push(block_close);
}, },
ClassDirective(node) { ClassDirective(node) {
error(node, 'INTERNAL', 'Node should have been handled elsewhere'); error(node, 'INTERNAL', 'Node should have been handled elsewhere');
@ -1417,9 +1428,7 @@ const template_visitors = {
} }
}; };
const [el_anchor, anchor_id] = serialize_anchor(context.state); context.state.template.push(block_open);
context.state.init.push(el_anchor);
context.state.template.push(t_expression(anchor_id));
const main = create_block(node, node.fragment.nodes, { const main = create_block(node, node.fragment.nodes, {
...context, ...context,
@ -1455,7 +1464,7 @@ const template_visitors = {
) )
) )
), ),
t_expression(anchor_id) block_close
); );
if (context.state.options.dev) { if (context.state.options.dev) {
context.state.template.push(t_statement(b.stmt(b.call('$.pop_element')))); context.state.template.push(t_statement(b.stmt(b.call('$.pop_element'))));
@ -1463,9 +1472,7 @@ const template_visitors = {
}, },
EachBlock(node, context) { EachBlock(node, context) {
const state = context.state; const state = context.state;
const [dec, id] = serialize_anchor(state); state.template.push(block_open);
state.init.push(dec);
state.template.push(t_expression(id));
const each_node_meta = node.metadata; const each_node_meta = node.metadata;
const collection = /** @type {import('estree').Expression} */ (context.visit(node.expression)); const collection = /** @type {import('estree').Expression} */ (context.visit(node.expression));
@ -1476,14 +1483,6 @@ const template_visitors = {
: b.id(node.index); : b.id(node.index);
const children = node.body.nodes; const children = node.body.nodes;
const [each_dec, each_id] = serialize_anchor(state);
/** @type {import('./types').Anchor} */
const anchor = {
type: 'Anchor',
id: each_id
};
const array_id = state.scope.root.unique('each_array'); const array_id = state.scope.root.unique('each_array');
state.init.push(b.const(array_id, b.call('$.ensure_array_like', collection))); state.init.push(b.const(array_id, b.call('$.ensure_array_like', collection)));
@ -1497,74 +1496,64 @@ const template_visitors = {
each.push(b.let(node.index, index)); each.push(b.let(node.index, index));
} }
each.push(b.stmt(b.assignment('+=', b.id('$$payload.out'), b.literal(block_open.value))));
each.push( each.push(
each_dec, .../** @type {import('estree').Statement[]} */ (create_block(node, children, context))
.../** @type {import('estree').Statement[]} */ (create_block(node, children, context, anchor))
); );
each.push(b.stmt(b.assignment('+=', b.id('$$payload.out'), b.literal(block_close.value))));
const for_loop = b.for( const for_loop = b.for(
b.let(index, b.literal(0)), b.let(index, b.literal(0)),
b.binary('<', index, b.member(array_id, b.id('length'))), b.binary('<', index, b.member(array_id, b.id('length'))),
b.update('++', index, false), b.update('++', index, false),
b.block(each) b.block(each)
); );
const close = b.stmt(b.assignment('+=', b.id('$$payload.out'), b.literal(BLOCK_CLOSE)));
if (node.fallback) { if (node.fallback) {
const fallback_stmts = create_block(node, node.fallback.nodes, context); const fallback = create_block(node, node.fallback.nodes, context);
fallback_stmts.unshift(
b.stmt(b.assignment('+=', b.id('$$payload.out'), b.literal('<!--ssr:each_else-->'))) fallback.push(b.stmt(b.assignment('+=', b.id('$$payload.out'), b.literal(BLOCK_CLOSE_ELSE))));
);
state.template.push( state.template.push(
t_statement( t_statement(
b.if( b.if(
b.binary('!==', b.member(array_id, b.id('length')), b.literal(0)), b.binary('!==', b.member(array_id, b.id('length')), b.literal(0)),
for_loop, b.block([for_loop, close]),
b.block(fallback_stmts) b.block(fallback)
) )
) )
); );
} else { } else {
state.template.push(t_statement(for_loop)); state.template.push(t_statement(for_loop), t_statement(close));
} }
state.template.push(t_expression(id));
}, },
IfBlock(node, context) { IfBlock(node, context) {
const state = context.state; const state = context.state;
const [dec, id] = serialize_anchor(state); state.template.push(block_open);
state.init.push(dec);
state.template.push(t_expression(id));
// Insert ssr:if:true/false anchors in addition to the other anchors so that
// the if block can catch hydration mismatches (false on the server, true on the client and vice versa)
// and continue hydration without having to re-render everything from scratch.
const consequent = create_block(node, node.consequent.nodes, context); const consequent = create_block(node, node.consequent.nodes, context);
consequent.unshift( const alternate = node.alternate ? create_block(node, node.alternate.nodes, context) : [];
b.stmt(b.assignment('+=', b.id('$$payload.out'), b.literal('<!--ssr:if:true-->')))
);
const alternate = node.alternate consequent.push(b.stmt(b.assignment('+=', b.id('$$payload.out'), b.literal(BLOCK_CLOSE))));
? /** @type {import('estree').BlockStatement} */ (context.visit(node.alternate)) alternate.push(b.stmt(b.assignment('+=', b.id('$$payload.out'), b.literal(BLOCK_CLOSE_ELSE))));
: b.block([]);
alternate.body.unshift(
b.stmt(b.assignment('+=', b.id('$$payload.out'), b.literal('<!--ssr:if:false-->')))
);
state.template.push( state.template.push(
t_statement( t_statement(
b.if( b.if(
/** @type {import('estree').Expression} */ (context.visit(node.test)), /** @type {import('estree').Expression} */ (context.visit(node.test)),
b.block(/** @type {import('estree').Statement[]} */ (consequent)), b.block(consequent),
alternate b.block(alternate)
) )
) )
); );
state.template.push(t_expression(id));
}, },
AwaitBlock(node, context) { AwaitBlock(node, context) {
const state = context.state; const state = context.state;
const [dec, id] = serialize_anchor(state); state.template.push(block_open);
state.init.push(dec);
state.template.push(t_expression(id));
state.template.push( state.template.push(
t_statement( t_statement(
@ -1598,16 +1587,14 @@ const template_visitors = {
) )
); );
state.template.push(t_expression(id)); state.template.push(block_close);
}, },
KeyBlock(node, context) { KeyBlock(node, context) {
const state = context.state; const state = context.state;
const [dec, id] = serialize_anchor(state); state.template.push(block_open);
state.init.push(dec);
state.template.push(t_expression(id));
const body = create_block(node, node.fragment.nodes, context); const body = create_block(node, node.fragment.nodes, context);
state.template.push(t_statement(b.block(body))); state.template.push(t_statement(b.block(body)));
state.template.push(t_expression(id)); state.template.push(block_close);
}, },
SnippetBlock(node, context) { SnippetBlock(node, context) {
// TODO hoist where possible // TODO hoist where possible
@ -1623,34 +1610,28 @@ const template_visitors = {
}, },
Component(node, context) { Component(node, context) {
const state = context.state; const state = context.state;
const [dec, id] = serialize_anchor(state); state.template.push(block_open);
state.init.push(dec);
state.template.push(t_expression(id));
const call = serialize_inline_component(node, node.name, context); const call = serialize_inline_component(node, node.name, context);
state.template.push(t_statement(call)); state.template.push(t_statement(call));
state.template.push(t_expression(id)); state.template.push(block_close);
}, },
SvelteSelf(node, context) { SvelteSelf(node, context) {
const state = context.state; const state = context.state;
const [dec, id] = serialize_anchor(state); state.template.push(block_open);
state.init.push(dec);
state.template.push(t_expression(id));
const call = serialize_inline_component(node, context.state.analysis.name, context); const call = serialize_inline_component(node, context.state.analysis.name, context);
state.template.push(t_statement(call)); state.template.push(t_statement(call));
state.template.push(t_expression(id)); state.template.push(block_close);
}, },
SvelteComponent(node, context) { SvelteComponent(node, context) {
const state = context.state; const state = context.state;
const [dec, id] = serialize_anchor(state); state.template.push(block_open);
state.init.push(dec);
state.template.push(t_expression(id));
const call = serialize_inline_component( const call = serialize_inline_component(
node, node,
/** @type {import('estree').Expression} */ (context.visit(node.expression)), /** @type {import('estree').Expression} */ (context.visit(node.expression)),
context context
); );
state.template.push(t_statement(call)); state.template.push(t_statement(call));
state.template.push(t_expression(id)); state.template.push(block_close);
}, },
LetDirective(node, { state }) { LetDirective(node, { state }) {
if (node.expression && node.expression.type !== 'Identifier') { if (node.expression && node.expression.type !== 'Identifier') {
@ -1733,9 +1714,7 @@ const template_visitors = {
}, },
SlotElement(node, context) { SlotElement(node, context) {
const state = context.state; const state = context.state;
const [dec, id] = serialize_anchor(state); state.template.push(block_open);
state.init.push(dec);
state.template.push(t_expression(id));
/** @type {import('estree').Property[]} */ /** @type {import('estree').Property[]} */
const props = []; const props = [];
@ -1782,7 +1761,7 @@ const template_visitors = {
const slot = b.call('$.slot', b.id('$$payload'), expression, props_expression, fallback); const slot = b.call('$.slot', b.id('$$payload'), expression, props_expression, fallback);
state.template.push(t_statement(b.stmt(slot))); state.template.push(t_statement(b.stmt(slot)));
state.template.push(t_expression(id)); state.template.push(block_close);
}, },
SvelteHead(node, context) { SvelteHead(node, context) {
const state = context.state; const state = context.state;
@ -2220,7 +2199,8 @@ export function server_component(analysis, options) {
}); });
} }
// If the component binds to a child, we need to put the template in a loop and repeat until bindings are stable // If the component binds to a child, we need to put the template in a loop and repeat until legacy bindings are stable.
// We can remove this once the legacy syntax is gone.
if (analysis.uses_component_bindings) { if (analysis.uses_component_bindings) {
template.body = [ template.body = [
b.let('$$settled', b.true), b.let('$$settled', b.true),
@ -2261,7 +2241,7 @@ export function server_component(analysis, options) {
/** @type {import('estree').Property[]} */ /** @type {import('estree').Property[]} */
const props = []; const props = [];
for (const [name, binding] of analysis.instance.scope.declarations) { for (const [name, binding] of analysis.instance.scope.declarations) {
if (binding.kind === 'prop' && !name.startsWith('$$')) { if (binding.kind === 'bindable_prop' && !name.startsWith('$$')) {
props.push(b.init(binding.prop_alias ?? name, b.id(name))); props.push(b.init(binding.prop_alias ?? name, b.id(name)));
} }
} }
@ -2269,6 +2249,8 @@ export function server_component(analysis, options) {
props.push(b.init(alias ?? name, b.id(name))); props.push(b.init(alias ?? name, b.id(name)));
} }
if (props.length > 0) { if (props.length > 0) {
// This has no effect in runes mode other than throwing an error when someone passes
// undefined to a binding that has a default value.
template.body.push(b.stmt(b.call('$.bind_props', b.id('$$props'), b.object(props)))); template.body.push(b.stmt(b.call('$.bind_props', b.id('$$props'), b.object(props))));
} }
@ -2283,7 +2265,7 @@ export function server_component(analysis, options) {
/** @type {string[]} */ /** @type {string[]} */
const named_props = analysis.exports.map(({ name, alias }) => alias ?? name); const named_props = analysis.exports.map(({ name, alias }) => alias ?? name);
for (const [name, binding] of analysis.instance.scope.declarations) { for (const [name, binding] of analysis.instance.scope.declarations) {
if (binding.kind === 'prop') named_props.push(binding.prop_alias ?? name); if (binding.kind === 'bindable_prop') named_props.push(binding.prop_alias ?? name);
} }
component_block.body.unshift( component_block.body.unshift(

@ -182,8 +182,6 @@ export const binding_properties = {
valid_elements: ['input', 'textarea', 'select'] valid_elements: ['input', 'textarea', 'select']
}, },
files: { files: {
event: 'change',
type: 'set',
valid_elements: ['input'], valid_elements: ['input'],
omit_in_ssr: true omit_in_ssr: true
} }

@ -32,6 +32,7 @@ export const Runes = /** @type {const} */ ([
'$state', '$state',
'$state.frozen', '$state.frozen',
'$props', '$props',
'$bindable',
'$derived', '$derived',
'$derived.by', '$derived.by',
'$effect', '$effect',

@ -23,8 +23,8 @@ export interface Template {
} }
export interface ReactiveStatement { export interface ReactiveStatement {
assignments: Set<Identifier>; assignments: Set<Binding>;
dependencies: Set<Binding>; dependencies: Binding[];
} }
export interface RawWarning { export interface RawWarning {
@ -73,6 +73,7 @@ export interface ComponentAnalysis extends Analysis {
hash: string; hash: string;
keyframes: string[]; keyframes: string[];
}; };
source: string;
} }
declare module 'estree' { declare module 'estree' {

@ -241,7 +241,8 @@ export interface Binding {
node: Identifier; node: Identifier;
/** /**
* - `normal`: A variable that is not in any way special * - `normal`: A variable that is not in any way special
* - `prop`: A normal prop (possibly mutated) * - `prop`: A normal prop (possibly reassigned or mutated)
* - `bindable_prop`: A prop one can `bind:` to (possibly reassigned or mutated)
* - `rest_prop`: A rest prop * - `rest_prop`: A rest prop
* - `state`: A state variable * - `state`: A state variable
* - `derived`: A derived variable * - `derived`: A derived variable
@ -253,6 +254,7 @@ export interface Binding {
kind: kind:
| 'normal' | 'normal'
| 'prop' | 'prop'
| 'bindable_prop'
| 'rest_prop' | 'rest_prop'
| 'state' | 'state'
| 'frozen_state' | 'frozen_state'
@ -280,7 +282,7 @@ export interface Binding {
scope: Scope; scope: Scope;
/** For `legacy_reactive`: its reactive dependencies */ /** For `legacy_reactive`: its reactive dependencies */
legacy_dependencies: Binding[]; legacy_dependencies: Binding[];
/** Legacy props: the `class` in `{ export klass as class}` */ /** Legacy props: the `class` in `{ export klass as class}`. $props(): The `class` in { class: klass } = $props() */
prop_alias: string | null; prop_alias: string | null;
/** /**
* If this is set, all references should use this expression instead of the identifier name. * If this is set, all references should use this expression instead of the identifier name.

@ -17,6 +17,15 @@ export function array_pattern(elements) {
return { type: 'ArrayPattern', elements }; return { type: 'ArrayPattern', elements };
} }
/**
* @param {import('estree').Pattern} left
* @param {import('estree').Expression} right
* @returns {import('estree').AssignmentPattern}
*/
export function assignment_pattern(left, right) {
return { type: 'AssignmentPattern', left, right };
}
/** /**
* @param {Array<import('estree').Pattern>} params * @param {Array<import('estree').Pattern>} params
* @param {import('estree').BlockStatement | import('estree').Expression} body * @param {import('estree').BlockStatement | import('estree').Expression} body

@ -412,7 +412,7 @@ export function merge_with_preprocessor_map(result, options, source_name) {
* @param {string} from * @param {string} from
* @param {string} to * @param {string} to
*/ */
export function get_relative_path(from, to) { function get_relative_path(from, to) {
// Don't use node's utils here to ensure the compiler is usable in a browser environment // Don't use node's utils here to ensure the compiler is usable in a browser environment
const from_parts = from.split(/[/\\]/); const from_parts = from.split(/[/\\]/);
const to_parts = to.split(/[/\\]/); const to_parts = to.split(/[/\\]/);

@ -16,6 +16,15 @@ export const TRANSITION_IN = 1;
export const TRANSITION_OUT = 1 << 1; export const TRANSITION_OUT = 1 << 1;
export const TRANSITION_GLOBAL = 1 << 2; export const TRANSITION_GLOBAL = 1 << 2;
export const TEMPLATE_FRAGMENT = 1;
export const TEMPLATE_USE_IMPORT_NODE = 1 << 1;
export const HYDRATION_START = '[';
export const HYDRATION_END = ']';
export const HYDRATION_END_ELSE = `${HYDRATION_END}!`; // used to indicate that an `{:else}...` block was rendered
export const UNINITIALIZED = Symbol();
/** List of Element events that will be delegated */ /** List of Element events that will be delegated */
export const DelegatedEvents = [ export const DelegatedEvents = [
'beforeinput', 'beforeinput',
@ -92,7 +101,6 @@ 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';
// while `input` is also an interactive element, it is never moved by the browser, so we don't need to check for it // 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([ export const interactive_elements = new Set([

@ -1,6 +1,6 @@
import { current_component_context, untrack } from '../internal/client/runtime.js'; import { current_component_context, flush_sync, untrack } from './internal/client/runtime.js';
import { is_array } from '../internal/client/utils.js'; import { is_array } from './internal/client/utils.js';
import { user_effect } from '../internal/index.js'; import { user_effect } from './internal/client/index.js';
/** /**
* The `onMount` function schedules a callback to run as soon as the component has been mounted to the DOM. * The `onMount` function schedules a callback to run as soon as the component has been mounted to the DOM.
@ -13,7 +13,7 @@ import { user_effect } from '../internal/index.js';
* *
* https://svelte.dev/docs/svelte#onmount * https://svelte.dev/docs/svelte#onmount
* @template T * @template T
* @param {() => import('./private.js').NotFunction<T> | Promise<import('./private.js').NotFunction<T>> | (() => any)} fn * @param {() => import('./internal/types').NotFunction<T> | Promise<import('./internal/types').NotFunction<T>> | (() => any)} fn
* @returns {void} * @returns {void}
*/ */
export function onMount(fn) { export function onMount(fn) {
@ -81,7 +81,7 @@ function create_custom_event(type, detail, { bubbles = false, cancelable = false
* *
* https://svelte.dev/docs/svelte#createeventdispatcher * https://svelte.dev/docs/svelte#createeventdispatcher
* @template {Record<string, any>} [EventMap = any] * @template {Record<string, any>} [EventMap = any]
* @returns {import('./public.js').EventDispatcher<EventMap>} * @returns {import('./index.js').EventDispatcher<EventMap>}
*/ */
export function createEventDispatcher() { export function createEventDispatcher() {
const component_context = current_component_context; const component_context = current_component_context;
@ -161,25 +161,30 @@ export function afterUpdate(fn) {
/** /**
* Legacy-mode: Init callbacks object for onMount/beforeUpdate/afterUpdate * Legacy-mode: Init callbacks object for onMount/beforeUpdate/afterUpdate
* @param {import('../internal/client/types.js').ComponentContext} context * @param {import('./internal/client/types.js').ComponentContext} context
*/ */
function init_update_callbacks(context) { function init_update_callbacks(context) {
return (context.u ??= { a: [], b: [], m: [] }); return (context.u ??= { a: [], b: [], m: [] });
} }
// TODO bring implementations in here /**
// (except probably untrack — do we want to expose that, if there's also a rune?) * Synchronously flushes any pending state changes and those that result from it.
* @param {() => void} [fn]
* @returns {void}
*/
export function flushSync(fn) {
flush_sync(fn);
}
export { unstate } from './internal/client/proxy.js';
export { hydrate, mount, unmount } from './internal/client/render.js';
export { export {
flushSync,
mount,
hydrate,
tick,
unmount,
untrack,
unstate,
createRoot,
hasContext,
getContext, getContext,
getAllContexts, getAllContexts,
setContext hasContext,
} from '../internal/index.js'; setContext,
tick,
untrack
} from './internal/client/runtime.js';

@ -0,0 +1,46 @@
import { current_component } from './internal/server/context.js';
import { noop } from './internal/shared/utils.js';
/** @param {() => void} fn */
export function onDestroy(fn) {
var context = /** @type {import('#server').Component} */ (current_component);
(context.d ??= []).push(fn);
}
export {
noop as beforeUpdate,
noop as afterUpdate,
noop as onMount,
noop as flushSync,
run as untrack
} from './internal/shared/utils.js';
export function createEventDispatcher() {
return noop;
}
export function mount() {
throw new Error('mount(...) is not available on the server');
}
export function hydrate() {
throw new Error('hydrate(...) is not available on the server');
}
export function unmount() {
throw new Error('unmount(...) is not available on the server');
}
export async function tick() {}
/**
* @template T
* @param {T} value
* @returns {T}
*/
export function unstate(value) {
// There's no signals/proxies on the server, so just return the value
return value;
}
export { getAllContexts, getContext, hasContext, setContext } from './internal/server/context.js';

@ -222,5 +222,5 @@ export interface EventDispatcher<EventMap extends Record<string, any>> {
): boolean; ): boolean;
} }
export * from './main-client.js'; export * from './index-client.js';
import './ambient.js'; import './ambient.js';

@ -1,8 +1,9 @@
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 RENDER_EFFECT = 1 << 3;
export const RENDER_EFFECT = 1 << 4; export const BLOCK_EFFECT = 1 << 4;
export const MANAGED = 1 << 6; export const BRANCH_EFFECT = 1 << 5;
export const ROOT_EFFECT = 1 << 6;
export const UNOWNED = 1 << 7; export const UNOWNED = 1 << 7;
export const CLEAN = 1 << 8; export const CLEAN = 1 << 8;
export const DIRTY = 1 << 9; export const DIRTY = 1 << 9;
@ -12,5 +13,4 @@ export const DESTROYED = 1 << 12;
export const IS_ELSEIF = 1 << 13; export const IS_ELSEIF = 1 << 13;
export const EFFECT_RAN = 1 << 14; export const EFFECT_RAN = 1 << 14;
export const UNINITIALIZED = Symbol();
export const STATE_SYMBOL = Symbol('$state'); export const STATE_SYMBOL = Symbol('$state');

@ -9,7 +9,7 @@ const boundaries = {};
const chrome_pattern = /at (?:.+ \()?(.+):(\d+):(\d+)\)?$/; const chrome_pattern = /at (?:.+ \()?(.+):(\d+):(\d+)\)?$/;
const firefox_pattern = /@(.+):(\d+):(\d+)$/; const firefox_pattern = /@(.+):(\d+):(\d+)$/;
export function get_stack() { function get_stack() {
const stack = new Error().stack; const stack = new Error().stack;
if (!stack) return null; if (!stack) return null;

@ -1,16 +1,13 @@
import { is_promise } from '../../../common.js'; import { is_promise } from '../../../shared/utils.js';
import { hydrate_block_anchor } from '../hydration.js';
import { remove } from '../reconciler.js';
import { import {
current_component_context, current_component_context,
flushSync, flush_sync,
set_current_component_context, set_current_component_context,
set_current_effect, set_current_effect,
set_current_reaction set_current_reaction
} from '../../runtime.js'; } from '../../runtime.js';
import { destroy_effect, pause_effect, render_effect } from '../../reactivity/effects.js'; import { block, branch, destroy_effect, pause_effect } from '../../reactivity/effects.js';
import { DESTROYED, INERT } from '../../constants.js'; import { INERT } from '../../constants.js';
import { create_block } from './utils.js';
/** /**
* @template V * @template V
@ -22,12 +19,8 @@ import { create_block } from './utils.js';
* @returns {void} * @returns {void}
*/ */
export function await_block(anchor, get_input, pending_fn, then_fn, catch_fn) { export function await_block(anchor, get_input, pending_fn, then_fn, catch_fn) {
const block = create_block();
const component_context = current_component_context; const component_context = current_component_context;
hydrate_block_anchor(anchor);
/** @type {any} */ /** @type {any} */
let input; let input;
@ -45,34 +38,22 @@ export function await_block(anchor, get_input, pending_fn, then_fn, catch_fn) {
* @param {any} value * @param {any} value
*/ */
function create_effect(fn, value) { function create_effect(fn, value) {
set_current_effect(branch); set_current_effect(effect);
set_current_reaction(branch); // TODO do we need both? set_current_reaction(effect); // TODO do we need both?
set_current_component_context(component_context); set_current_component_context(component_context);
var effect = render_effect(() => fn(anchor, value), {}, true); var e = branch(() => fn(anchor, value));
set_current_component_context(null); set_current_component_context(null);
set_current_reaction(null); set_current_reaction(null);
set_current_effect(null); set_current_effect(null);
// without this, the DOM does not update until two ticks after the promise, // without this, the DOM does not update until two ticks after the promise,
// resolves which is unexpected behaviour (and somewhat irksome to test) // resolves which is unexpected behaviour (and somewhat irksome to test)
flushSync(); flush_sync();
return effect; return e;
} }
/** @param {import('#client').Effect} effect */ const effect = block(() => {
function pause(effect) {
if ((effect.f & DESTROYED) !== 0) return;
const block = effect.block;
pause_effect(effect, () => {
// TODO make this unnecessary
const dom = block?.d;
if (dom) remove(dom);
});
}
const branch = render_effect(() => {
if (input === (input = get_input())) return; if (input === (input = get_input())) return;
if (is_promise(input)) { if (is_promise(input)) {
@ -80,20 +61,19 @@ export function await_block(anchor, get_input, pending_fn, then_fn, catch_fn) {
if (pending_fn) { if (pending_fn) {
if (pending_effect && (pending_effect.f & INERT) === 0) { if (pending_effect && (pending_effect.f & INERT) === 0) {
if (pending_effect.block?.d) remove(pending_effect.block.d);
destroy_effect(pending_effect); destroy_effect(pending_effect);
} }
pending_effect = render_effect(() => pending_fn(anchor), {}, true); pending_effect = branch(() => pending_fn(anchor));
} }
if (then_effect) pause(then_effect); if (then_effect) pause_effect(then_effect);
if (catch_effect) pause(catch_effect); if (catch_effect) pause_effect(catch_effect);
promise.then( promise.then(
(value) => { (value) => {
if (promise !== input) return; if (promise !== input) return;
if (pending_effect) pause(pending_effect); if (pending_effect) pause_effect(pending_effect);
if (then_fn) { if (then_fn) {
then_effect = create_effect(then_fn, value); then_effect = create_effect(then_fn, value);
@ -101,7 +81,7 @@ export function await_block(anchor, get_input, pending_fn, then_fn, catch_fn) {
}, },
(error) => { (error) => {
if (promise !== input) return; if (promise !== input) return;
if (pending_effect) pause(pending_effect); if (pending_effect) pause_effect(pending_effect);
if (catch_fn) { if (catch_fn) {
catch_effect = create_effect(catch_fn, error); catch_effect = create_effect(catch_fn, error);
@ -109,24 +89,16 @@ export function await_block(anchor, get_input, pending_fn, then_fn, catch_fn) {
} }
); );
} else { } else {
if (pending_effect) pause(pending_effect); if (pending_effect) pause_effect(pending_effect);
if (catch_effect) pause(catch_effect); if (catch_effect) pause_effect(catch_effect);
if (then_fn) { if (then_fn) {
if (then_effect) { if (then_effect) {
if (then_effect.block?.d) remove(then_effect.block.d);
destroy_effect(then_effect); destroy_effect(then_effect);
} }
then_effect = render_effect(() => then_fn(anchor, input), {}, true); then_effect = branch(() => then_fn(anchor, input));
} }
} }
}, block); });
branch.ondestroy = () => {
// TODO this sucks, tidy it up
if (pending_effect?.block?.d) remove(pending_effect.block.d);
if (then_effect?.block?.d) remove(then_effect.block.d);
if (catch_effect?.block?.d) remove(catch_effect.block.d);
};
} }

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

@ -4,75 +4,107 @@ import {
EACH_IS_CONTROLLED, EACH_IS_CONTROLLED,
EACH_IS_STRICT_EQUALS, EACH_IS_STRICT_EQUALS,
EACH_ITEM_REACTIVE, EACH_ITEM_REACTIVE,
EACH_KEYED EACH_KEYED,
HYDRATION_END_ELSE,
HYDRATION_START
} from '../../../../constants.js'; } from '../../../../constants.js';
import { import { hydrate_anchor, hydrate_nodes, hydrating, set_hydrating } from '../hydration.js';
current_hydration_fragment,
get_hydration_fragment,
hydrate_block_anchor,
hydrating,
set_current_hydration_fragment
} from '../hydration.js';
import { empty } from '../operations.js'; import { empty } from '../operations.js';
import { insert, remove } from '../reconciler.js'; import { remove } from '../reconciler.js';
import { untrack } from '../../runtime.js'; import { untrack } from '../../runtime.js';
import { import {
block,
branch,
destroy_effect, destroy_effect,
effect,
run_out_transitions,
pause_children,
pause_effect, pause_effect,
render_effect, resume_effect
resume_effect,
user_effect
} from '../../reactivity/effects.js'; } from '../../reactivity/effects.js';
import { source, mutable_source, set } from '../../reactivity/sources.js'; import { source, mutable_source, set } from '../../reactivity/sources.js';
import { is_array, is_frozen, map_get, map_set } from '../../utils.js'; import { is_array, is_frozen, map_get, map_set } from '../../utils.js';
import { STATE_SYMBOL } from '../../constants.js'; import { STATE_SYMBOL } from '../../constants.js';
import { create_block } from './utils.js';
var NEW_BLOCK = -1; var NEW_ITEM = -1;
var LIS_BLOCK = -2; var LIS_ITEM = -2;
/** /**
* The row of a keyed each block that is currently updating. We track this * The row of a keyed each block that is currently updating. We track this
* so that `animate:` directives have something to attach themselves to * so that `animate:` directives have something to attach themselves to
* @type {import('#client').EachItem | null} * @type {import('#client').EachItem | null}
*/ */
export let current_each_item_block = null; export let current_each_item = null;
/** @param {import('#client').EachItem | null} item */
export function set_current_each_item(item) {
current_each_item = item;
}
/**
* Pause multiple effects simultaneously, and coordinate their
* subsequent destruction. Used in each blocks
* @param {import('#client').Effect[]} effects
* @param {null | Node} controlled_anchor
* @param {() => void} [callback]
*/
function pause_effects(effects, controlled_anchor, callback) {
/** @type {import('#client').TransitionManager[]} */
var transitions = [];
var length = effects.length;
for (var i = 0; i < length; i++) {
pause_children(effects[i], transitions, true);
}
// If we have a controlled anchor, it means that the each block is inside a single
// DOM element, so we can apply a fast-path for clearing the contents of the element.
if (effects.length > 0 && transitions.length === 0 && controlled_anchor !== null) {
var parent_node = /** @type {Element} */ (controlled_anchor.parentNode);
parent_node.textContent = '';
parent_node.append(controlled_anchor);
}
run_out_transitions(transitions, () => {
for (var i = 0; i < length; i++) {
destroy_effect(effects[i]);
}
/** @param {import('#client').EachItem | null} block */ if (callback !== undefined) callback();
export function set_current_each_item_block(block) { });
current_each_item_block = block;
} }
/** /**
* @template V * @template V
* @param {Element | Comment} anchor The next sibling node, or the parent node if this is a 'controlled' block * @param {Element | Comment} anchor The next sibling node, or the parent node if this is a 'controlled' block
* @param {() => V[]} get_collection
* @param {number} flags * @param {number} flags
* @param {() => V[]} get_collection
* @param {null | ((item: V) => string)} get_key * @param {null | ((item: V) => string)} get_key
* @param {(anchor: null, item: V, index: import('#client').MaybeSource<number>) => void} render_fn * @param {(anchor: Node, item: import('#client').MaybeSource<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}
*/ */
function each(anchor, get_collection, flags, get_key, render_fn, fallback_fn, reconcile_fn) { function each(anchor, flags, get_collection, get_key, render_fn, fallback_fn, reconcile_fn) {
var block = create_block();
/** @type {import('#client').EachState} */ /** @type {import('#client').EachState} */
var state = { flags, items: [] }; var state = { flags, items: [] };
var is_controlled = (flags & EACH_IS_CONTROLLED) !== 0; var is_controlled = (flags & EACH_IS_CONTROLLED) !== 0;
hydrate_block_anchor(is_controlled ? /** @type {Node} */ (anchor.firstChild) : anchor);
if (is_controlled) { if (is_controlled) {
var parent_node = /** @type {Element} */ (anchor); var parent_node = /** @type {Element} */ (anchor);
parent_node.append((anchor = empty()));
anchor = hydrating
? /** @type {Comment | Text} */ (
hydrate_anchor(/** @type {Comment | Text} */ (parent_node.firstChild))
)
: parent_node.appendChild(empty());
} }
/** @type {import('#client').Effect | null} */ /** @type {import('#client').Effect | null} */
var fallback = null; var fallback = null;
var effect = render_effect( block(() => {
() => {
var collection = get_collection(); var collection = get_collection();
var array = is_array(collection) var array = is_array(collection)
@ -101,57 +133,53 @@ function each(anchor, get_collection, flags, get_key, render_fn, fallback_fn, re
let mismatch = false; let mismatch = false;
if (hydrating) { if (hydrating) {
var is_else = var is_else = /** @type {Comment} */ (anchor).data === HYDRATION_END_ELSE;
/** @type {Comment} */ (current_hydration_fragment?.[0])?.data === 'ssr:each_else';
if (is_else !== (length === 0)) { if (is_else !== (length === 0)) {
// hydration mismatch — remove the server-rendered DOM and start over // hydration mismatch — remove the server-rendered DOM and start over
remove(current_hydration_fragment); remove(hydrate_nodes);
set_current_hydration_fragment(null); set_hydrating(false);
mismatch = true; mismatch = true;
} else if (is_else) {
// Remove the each_else comment node or else it will confuse the subsequent hydration algorithm
/** @type {import('#client').TemplateNode[]} */ (current_hydration_fragment).shift();
} }
} }
// this is separate to the previous block because `hydrating` might change // this is separate to the previous block because `hydrating` might change
if (hydrating) { if (hydrating) {
var b_blocks = []; var b_items = [];
// Hydrate block /** @type {Node} */
var hydration_list = /** @type {import('#client').TemplateNode[]} */ ( var child_anchor = hydrate_nodes[0];
current_hydration_fragment
);
var hydrating_node = hydration_list[0];
for (var i = 0; i < length; i++) { for (var i = 0; i < length; i++) {
var fragment = get_hydration_fragment(hydrating_node); if (
set_current_hydration_fragment(fragment); child_anchor.nodeType !== 8 ||
if (!fragment) { /** @type {Comment} */ (child_anchor).data !== HYDRATION_START
// If fragment is null, then that means that the server rendered less items than what ) {
// the client code specifies -> break out and continue with client-side node creation // If `nodes` is null, then that means that the server rendered fewer items than what
// expected, so break out and continue appending non-hydrated items
mismatch = true; mismatch = true;
set_hydrating(false);
break; break;
} }
b_blocks[i] = create_item(array[i], keys?.[i], i, render_fn, flags); child_anchor = hydrate_anchor(child_anchor);
b_items[i] = create_item(child_anchor, array[i], keys?.[i], i, render_fn, flags);
// TODO helperise this child_anchor = /** @type {Comment} */ (child_anchor.nextSibling);
hydrating_node = /** @type {import('#client').TemplateNode} */ (
/** @type {Node} */ (
/** @type {Node} */ (fragment[fragment.length - 1] || hydrating_node).nextSibling
).nextSibling
);
} }
remove_excess_hydration_nodes(hydration_list, hydrating_node); // remove excess nodes
if (length > 0) {
while (child_anchor !== anchor) {
var next = /** @type {import('#client').TemplateNode} */ (child_anchor.nextSibling);
/** @type {import('#client').TemplateNode} */ (child_anchor).remove();
child_anchor = next;
}
}
state.items = b_blocks; state.items = b_items;
} }
if (!hydrating) { if (!hydrating) {
// TODO add 'empty controlled block' optimisation here
reconcile_fn(array, state, anchor, render_fn, flags, keys); reconcile_fn(array, state, anchor, render_fn, flags, keys);
} }
@ -160,21 +188,7 @@ function each(anchor, get_collection, flags, get_key, render_fn, fallback_fn, re
if (fallback) { if (fallback) {
resume_effect(fallback); resume_effect(fallback);
} else { } else {
fallback = render_effect( fallback = branch(() => fallback_fn(anchor));
() => {
fallback_fn(anchor);
var dom = block.d; // TODO would be nice if this was just returned from the managed effect function...
return () => {
if (dom !== null) {
remove(dom);
dom = null;
}
};
},
block,
true
);
} }
} else if (fallback !== null) { } else if (fallback !== null) {
pause_effect(fallback, () => { pause_effect(fallback, () => {
@ -184,51 +198,37 @@ function each(anchor, get_collection, flags, get_key, render_fn, fallback_fn, re
} }
if (mismatch) { if (mismatch) {
// Set a fragment so that Svelte continues to operate in hydration mode // continue in hydration mode
set_current_hydration_fragment([]); set_hydrating(true);
} }
}, });
block,
false
);
effect.ondestroy = () => {
for (var item of state.items) {
if (item.d !== null) {
destroy_effect(item.e);
remove(item.d);
}
}
if (fallback) destroy_effect(fallback);
};
} }
/** /**
* @template V * @template V
* @param {Element | Comment} anchor * @param {Element | Comment} anchor
* @param {() => V[]} get_collection
* @param {number} flags * @param {number} flags
* @param {() => V[]} get_collection
* @param {null | ((item: V) => string)} get_key * @param {null | ((item: V) => string)} get_key
* @param {(anchor: null, item: V, index: import('#client').MaybeSource<number>) => void} render_fn * @param {(anchor: Node, item: import('#client').MaybeSource<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}
*/ */
export function each_keyed(anchor, get_collection, flags, get_key, render_fn, fallback_fn) { export function each_keyed(anchor, flags, get_collection, get_key, render_fn, fallback_fn = null) {
each(anchor, get_collection, flags, get_key, render_fn, fallback_fn, reconcile_tracked_array); each(anchor, flags, get_collection, get_key, render_fn, fallback_fn, reconcile_tracked_array);
} }
/** /**
* @template V * @template V
* @param {Element | Comment} anchor * @param {Element | Comment} anchor
* @param {() => V[]} get_collection
* @param {number} flags * @param {number} flags
* @param {(anchor: null, item: V, index: import('#client').MaybeSource<number>) => void} render_fn * @param {() => V[]} get_collection
* @param {null | ((anchor: Node) => void)} fallback_fn * @param {(anchor: Node, item: import('#client').MaybeSource<V>, index: import('#client').MaybeSource<number>) => void} render_fn
* @param {null | ((anchor: Node) => void)} [fallback_fn]
* @returns {void} * @returns {void}
*/ */
export function each_indexed(anchor, get_collection, flags, render_fn, fallback_fn) { export function each_indexed(anchor, flags, get_collection, render_fn, fallback_fn = null) {
each(anchor, get_collection, flags, null, render_fn, fallback_fn, reconcile_indexed_array); each(anchor, flags, get_collection, null, render_fn, fallback_fn, reconcile_indexed_array);
} }
/** /**
@ -236,7 +236,7 @@ export function each_indexed(anchor, get_collection, flags, render_fn, fallback_
* @param {Array<V>} array * @param {Array<V>} array
* @param {import('#client').EachState} state * @param {import('#client').EachState} state
* @param {Element | Comment | Text} anchor * @param {Element | Comment | Text} anchor
* @param {(anchor: null, item: V, index: number | import('#client').Source<number>) => void} render_fn * @param {(anchor: Node, item: import('#client').MaybeSource<V>, index: number | import('#client').Source<number>) => void} render_fn
* @param {number} flags * @param {number} flags
* @returns {void} * @returns {void}
*/ */
@ -266,34 +266,23 @@ function reconcile_indexed_array(array, state, anchor, render_fn, flags) {
// add items // add items
for (; i < b; i += 1) { for (; i < b; i += 1) {
value = array[i]; value = array[i];
item = create_item(value, null, i, render_fn, flags); item = create_item(anchor, value, null, i, render_fn, flags);
b_items[i] = item; b_items[i] = item;
insert_item(item, anchor);
} }
state.items = b_items; state.items = b_items;
} else if (a > b) { } else if (a > b) {
// remove items // remove items
var remaining = a - b; var effects = [];
for (i = b; i < a; i += 1) {
var clear = () => { effects.push(a_items[i].e);
for (var i = b; i < a; i += 1) {
var block = a_items[i];
if (block.d) remove(block.d);
} }
state.items.length = b; var controlled_anchor = (flags & EACH_IS_CONTROLLED) !== 0 && b === 0 ? anchor : null;
};
var check = () => { pause_effects(effects, controlled_anchor, () => {
if (--remaining === 0) { state.items.length = b;
clear(); });
}
};
for (; i < a; i += 1) {
pause_effect(a_items[i].e, check);
}
} }
} }
@ -305,48 +294,59 @@ function reconcile_indexed_array(array, state, anchor, render_fn, flags) {
* @param {Array<V>} array * @param {Array<V>} array
* @param {import('#client').EachState} state * @param {import('#client').EachState} state
* @param {Element | Comment | Text} anchor * @param {Element | Comment | Text} anchor
* @param {(anchor: null, item: V, index: number | import('#client').Source<number>) => void} render_fn * @param {(anchor: Node, item: import('#client').MaybeSource<V>, index: number | import('#client').Source<number>) => void} render_fn
* @param {number} flags * @param {number} flags
* @param {any[]} keys * @param {any[]} keys
* @returns {void} * @returns {void}
*/ */
function reconcile_tracked_array(array, state, anchor, render_fn, flags, keys) { function reconcile_tracked_array(array, state, anchor, render_fn, flags, keys) {
var a_blocks = state.items; var a_items = state.items;
var a = a_blocks.length; var a = a_items.length;
var b = array.length; var b = array.length;
/** @type {Array<import('#client').EachItem>} */ /** @type {Array<import('#client').EachItem>} */
var b_blocks = Array(b); var b_items = Array(b);
var is_animated = (flags & EACH_IS_ANIMATED) !== 0; var is_animated = (flags & EACH_IS_ANIMATED) !== 0;
var should_update = (flags & (EACH_ITEM_REACTIVE | EACH_INDEX_REACTIVE)) !== 0; var should_update = (flags & (EACH_ITEM_REACTIVE | EACH_INDEX_REACTIVE)) !== 0;
var is_controlled = (flags & EACH_IS_CONTROLLED) !== 0;
var start = 0; var start = 0;
var block; var item;
/** @type {Array<import('#client').EachItem>} */ /** @type {import('#client').Effect[]} */
var to_destroy = []; var to_destroy = [];
/** @type {Array<import('#client').EachItem>} */
var to_animate = [];
// Step 1 — trim common suffix // Step 1 — trim common suffix
while (a > 0 && b > 0 && a_blocks[a - 1].k === keys[b - 1]) { while (a > 0 && b > 0 && a_items[a - 1].k === keys[b - 1]) {
block = b_blocks[--b] = a_blocks[--a]; item = b_items[--b] = a_items[--a];
anchor = get_first_child(block); anchor = get_first_child(item);
resume_effect(block.e); resume_effect(item.e);
if (should_update) { if (should_update) {
update_item(block, array[b], b, flags); update_item(item, array[b], b, flags);
}
if (is_animated) {
item.a?.measure();
to_animate.push(item);
} }
} }
// Step 2 — trim common prefix // Step 2 — trim common prefix
while (start < a && start < b && a_blocks[start].k === keys[start]) { while (start < a && start < b && a_items[start].k === keys[start]) {
block = b_blocks[start] = a_blocks[start]; item = b_items[start] = a_items[start];
resume_effect(block.e); resume_effect(item.e);
if (should_update) { if (should_update) {
update_item(block, array[start], start, flags); update_item(item, array[start], start, flags);
}
if (is_animated) {
item.a?.measure();
to_animate.push(item);
} }
start += 1; start += 1;
@ -356,14 +356,13 @@ function reconcile_tracked_array(array, state, anchor, render_fn, flags, keys) {
if (start === a) { if (start === a) {
// add only // add only
while (start < b) { while (start < b) {
block = create_item(array[start], keys[start], start, render_fn, flags); item = create_item(anchor, array[start], keys[start], start, render_fn, flags);
b_blocks[start++] = block; b_items[start++] = item;
insert_item(block, anchor);
} }
} else if (start === b) { } else if (start === b) {
// remove only // remove only
while (start < a) { while (start < a) {
to_destroy.push(a_blocks[start++]); to_destroy.push(a_items[start++].e);
} }
} else { } else {
// reconcile // reconcile
@ -372,78 +371,82 @@ function reconcile_tracked_array(array, state, anchor, render_fn, flags, keys) {
var indexes = new Map(); var indexes = new Map();
var i; var i;
var index; var index;
var last_block; var last_item;
var last_sibling;
// store the indexes of each block in the new world // store the indexes of each item in the new world
for (i = start; i < b; i += 1) { for (i = start; i < b; i += 1) {
sources[i - start] = NEW_BLOCK; sources[i - start] = NEW_ITEM;
map_set(indexes, keys[i], i); map_set(indexes, keys[i], i);
} }
/** @type {Array<import('#client').EachItem>} */
var to_animate = [];
if (is_animated) { if (is_animated) {
// for all blocks that were in both the old and the new list, // for all items that were in both the old and the new list,
// measure them and store them in `to_animate` so we can // measure them and store them in `to_animate` so we can
// apply animations once the DOM has been updated // apply animations once the DOM has been updated
for (i = 0; i < a_blocks.length; i += 1) { for (i = 0; i < a_items.length; i += 1) {
block = a_blocks[i]; item = a_items[i];
if (indexes.has(block.k)) { if (indexes.has(item.k)) {
block.a?.measure(); item.a?.measure();
to_animate.push(block); to_animate.push(item);
} }
} }
} }
// populate the `sources` array for each old block with // populate the `sources` array for each old item with
// its new index, so that we can calculate moves // its new index, so that we can calculate moves
for (i = start; i < a; i += 1) { for (i = start; i < a; i += 1) {
block = a_blocks[i]; item = a_items[i];
index = map_get(indexes, block.k); index = map_get(indexes, item.k);
resume_effect(block.e); resume_effect(item.e);
if (index === undefined) { if (index === undefined) {
to_destroy.push(block); to_destroy.push(item.e);
} else { } else {
moved = true; moved = true;
sources[index - start] = i; sources[index - start] = i;
b_blocks[index] = block; b_items[index] = item;
if (is_animated) { if (is_animated) {
to_animate.push(block); to_animate.push(item);
} }
} }
} }
// if we need to move blocks (as opposed to just adding/removing), // if we need to move items (as opposed to just adding/removing),
// figure out how to do so efficiently (I would be lying if I said // figure out how to do so efficiently (I would be lying if I said
// I fully understand this part) // I fully understand this part)
if (moved) { if (moved) {
mark_lis(sources); mark_lis(sources);
} else if (is_controlled && to_destroy.length === a_items.length) {
// We can optimize the case in which all items are replaced —
// destroy everything first, then append new items
pause_effects(to_destroy, anchor);
to_destroy = [];
} }
// working from the back, insert new or moved blocks // working from the back, insert new or moved items
while (b-- > start) { while (b-- > start) {
index = sources[b - start]; index = sources[b - start];
var insert = index === NEW_BLOCK; var should_insert = index === NEW_ITEM;
if (insert) { if (should_insert) {
block = create_item(array[b], keys[b], b, render_fn, flags); if (last_item !== undefined) anchor = get_first_child(last_item);
item = create_item(anchor, array[b], keys[b], b, render_fn, flags);
} else { } else {
block = b_blocks[b]; item = b_items[b];
if (should_update) { if (should_update) {
update_item(block, array[b], b, flags); update_item(item, array[b], b, flags);
}
} }
if (insert || (moved && index !== LIS_BLOCK)) { if (moved && index !== LIS_ITEM) {
last_sibling = last_block === undefined ? anchor : get_first_child(last_block); if (last_item !== undefined) anchor = get_first_child(last_item);
anchor = insert_item(block, last_sibling); move(/** @type {import('#client').Dom} */ (item.e.dom), anchor);
}
} }
last_block = b_blocks[b] = block; last_item = b_items[b] = item;
}
} }
if (to_animate.length > 0) { if (to_animate.length > 0) {
@ -451,52 +454,20 @@ function reconcile_tracked_array(array, state, anchor, render_fn, flags, keys) {
// we can figure out the eventual destination of the animating elements // we can figure out the eventual destination of the animating elements
// - https://github.com/sveltejs/svelte/pull/10798#issuecomment-2013681778 // - https://github.com/sveltejs/svelte/pull/10798#issuecomment-2013681778
// - https://svelte.dev/repl/6e891305e9644a7ca7065fa95c79d2d2?version=4.2.9 // - https://svelte.dev/repl/6e891305e9644a7ca7065fa95c79d2d2?version=4.2.9
user_effect(() => { effect(() => {
untrack(() => { untrack(() => {
for (block of to_animate) { for (item of to_animate) {
block.a?.apply(); item.a?.apply();
} }
}); });
}); });
} }
}
var remaining = to_destroy.length;
if (remaining > 0) {
var clear = () => {
for (block of to_destroy) {
if (block.d) remove(block.d);
}
state.items = b_blocks;
};
var check = () => { var controlled_anchor = is_controlled && b_items.length === 0 ? anchor : null;
if (--remaining === 0) {
clear();
}
};
for (block of to_destroy) { pause_effects(to_destroy, controlled_anchor, () => {
pause_effect(block.e, check); state.items = b_items;
} });
} else {
state.items = b_blocks;
}
}
/**
* The server could have rendered more list items than the client specifies.
* In that case, we need to remove the remaining server-rendered nodes.
* @param {import('#client').TemplateNode[]} hydration_list
* @param {import('#client').TemplateNode | null} next_node
*/
function remove_excess_hydration_nodes(hydration_list, next_node) {
if (next_node === null) return;
var idx = hydration_list.indexOf(next_node);
if (idx !== -1 && hydration_list.length > idx + 1) {
remove(hydration_list.slice(idx));
}
} }
/** /**
@ -524,7 +495,7 @@ function mark_lis(a) {
var hi; var hi;
// Skip -1 values at the start of the input array `a`. // Skip -1 values at the start of the input array `a`.
for (; a[i] === NEW_BLOCK; ++i) { for (; a[i] === NEW_ITEM; ++i) {
/**/ /**/
} }
@ -533,7 +504,7 @@ function mark_lis(a) {
for (; i < length; ++i) { for (; i < length; ++i) {
k = a[i]; k = a[i];
if (k !== NEW_BLOCK) { if (k !== NEW_ITEM) {
// Ignore -1 values. // Ignore -1 values.
j = index[index_length]; j = index[index_length];
@ -567,27 +538,17 @@ function mark_lis(a) {
j = index[index_length]; j = index[index_length];
while (index_length-- >= 0) { while (index_length-- >= 0) {
a[j] = LIS_BLOCK; a[j] = LIS_ITEM;
j = parent[j]; j = parent[j];
} }
} }
/** /**
* @param {import('#client').EachItem} block * @param {import('#client').EachItem} item
* @param {Text | Element | Comment} sibling
* @returns {Text | Element | Comment}
*/
function insert_item(block, sibling) {
var current = /** @type {import('#client').TemplateNode} */ (block.d);
return insert(current, sibling);
}
/**
* @param {import('#client').EachItem} block
* @returns {Text | Element | Comment} * @returns {Text | Element | Comment}
*/ */
function get_first_child(block) { function get_first_child(item) {
var current = block.d; var current = item.e.dom;
if (is_array(current)) { if (is_array(current)) {
return /** @type {Text | Element | Comment} */ (current[0]); return /** @type {Text | Element | Comment} */ (current[0]);
@ -597,73 +558,73 @@ function get_first_child(block) {
} }
/** /**
* @param {import('#client').EachItem} block * @param {import('#client').EachItem} item
* @param {any} item * @param {any} value
* @param {number} index * @param {number} index
* @param {number} type * @param {number} type
* @returns {void} * @returns {void}
*/ */
function update_item(block, item, index, type) { function update_item(item, value, index, type) {
if ((type & EACH_ITEM_REACTIVE) !== 0) { if ((type & EACH_ITEM_REACTIVE) !== 0) {
set(block.v, item); set(item.v, value);
} }
if ((type & EACH_INDEX_REACTIVE) !== 0) { if ((type & EACH_INDEX_REACTIVE) !== 0) {
set(/** @type {import('#client').Value<number>} */ (block.i), index); set(/** @type {import('#client').Value<number>} */ (item.i), index);
} else { } else {
block.i = index; item.i = index;
} }
} }
/** /**
* @template V * @template V
* @param {V} item * @param {Node} anchor
* @param {V} value
* @param {unknown} key * @param {unknown} key
* @param {number} index * @param {number} index
* @param {(anchor: null, item: V, index: number | import('#client').Value<number>) => void} render_fn * @param {(anchor: Node, item: V | import('#client').Source<V>, index: number | import('#client').Value<number>) => void} render_fn
* @param {number} flags * @param {number} flags
* @returns {import('#client').EachItem} * @returns {import('#client').EachItem}
*/ */
function create_item(item, key, index, render_fn, flags) { function create_item(anchor, value, key, index, render_fn, flags) {
var each_item_not_reactive = (flags & EACH_ITEM_REACTIVE) === 0; var previous_each_item = current_each_item;
var item_value = each_item_not_reactive try {
? item var reactive = (flags & EACH_ITEM_REACTIVE) !== 0;
: (flags & EACH_IS_STRICT_EQUALS) !== 0 var mutable = (flags & EACH_IS_STRICT_EQUALS) === 0;
? source(item)
: mutable_source(item); var v = reactive ? (mutable ? mutable_source(value) : source(value)) : value;
var i = (flags & EACH_INDEX_REACTIVE) === 0 ? index : source(index);
/** @type {import('#client').EachItem} */ /** @type {import('#client').EachItem} */
var block = { var item = {
i,
v,
k: key,
a: null, a: null,
// dom
d: null,
// effect
// @ts-expect-error // @ts-expect-error
e: null, e: null
// index
i: (flags & EACH_INDEX_REACTIVE) === 0 ? index : source(index),
// key
k: key,
// item
v: item_value
}; };
var previous_each_item_block = current_each_item_block; current_each_item = item;
item.e = branch(() => render_fn(anchor, v, i));
try {
current_each_item_block = block;
block.e = render_effect( return item;
() => {
render_fn(null, block.v, block.i);
},
block,
true
);
return block;
} finally { } finally {
current_each_item_block = previous_each_item_block; current_each_item = previous_each_item;
}
}
/**
* @param {import('#client').Dom} current
* @param {Text | Element | Comment} anchor
*/
function move(current, anchor) {
if (is_array(current)) {
for (var i = 0; i < current.length; i++) {
anchor.before(current[i]);
}
} else {
anchor.before(current);
} }
} }

@ -1,29 +1,66 @@
import { derived } from '../../reactivity/deriveds.js';
import { render_effect } from '../../reactivity/effects.js'; import { render_effect } from '../../reactivity/effects.js';
import { reconcile_html, remove } from '../reconciler.js'; import { get } from '../../runtime.js';
import { hydrate_nodes, hydrating } from '../hydration.js';
import { create_fragment_from_html, remove } from '../reconciler.js';
/** /**
* @param {Element | Text | Comment} dom * @param {Element | Text | Comment} anchor
* @param {() => string} get_value * @param {() => string} get_value
* @param {boolean} svg * @param {boolean} svg
* @returns {void} * @returns {void}
*/ */
export function html(dom, get_value, svg) { export function html(anchor, get_value, svg) {
/** @type {import('#client').TemplateNode | import('#client').TemplateNode[]} */ let value = derived(get_value);
let html_dom;
/** @type {string} */ render_effect(() => {
let value; var dom = html_to_dom(anchor, get(value), svg);
const effect = render_effect(() => { if (dom) {
if (value !== (value = get_value())) { return () => remove(dom);
if (html_dom) remove(html_dom);
html_dom = reconcile_html(dom, value, svg);
} }
}); });
}
/**
* Creates the content for a `@html` tag from its string value,
* inserts it before the target anchor and returns the new nodes.
* @template V
* @param {Element | Text | Comment} target
* @param {V} value
* @param {boolean} svg
* @returns {Element | Comment | (Element | Comment | Text)[]}
*/
function html_to_dom(target, value, svg) {
if (hydrating) return hydrate_nodes;
var html = value + '';
if (svg) html = `<svg>${html}</svg>`;
// Don't use create_fragment_with_script_from_html here because that would mean script tags are executed.
// @html is basically `.innerHTML = ...` and that doesn't execute scripts either due to security reasons.
/** @type {DocumentFragment | Element} */
var node = create_fragment_from_html(html);
effect.ondestroy = () => { if (svg) {
if (html_dom) { node = /** @type {Element} */ (node.firstChild);
remove(html_dom);
} }
};
if (node.childNodes.length === 1) {
var child = /** @type {Text | Element | Comment} */ (node.firstChild);
target.before(child);
return child;
}
var nodes = /** @type {Array<Text | Element | Comment>} */ ([...node.childNodes]);
if (svg) {
while (node.firstChild) {
target.before(node.firstChild);
}
} else {
target.before(node);
}
return nodes;
} }

@ -1,38 +1,24 @@
import { IS_ELSEIF } from '../../constants.js'; import { IS_ELSEIF } from '../../constants.js';
import { import { hydrate_nodes, hydrating, set_hydrating } from '../hydration.js';
current_hydration_fragment,
hydrate_block_anchor,
hydrating,
set_current_hydration_fragment
} from '../hydration.js';
import { remove } from '../reconciler.js'; import { remove } from '../reconciler.js';
import { import { block, branch, pause_effect, resume_effect } from '../../reactivity/effects.js';
destroy_effect, import { HYDRATION_END_ELSE } from '../../../../constants.js';
pause_effect,
render_effect,
resume_effect
} from '../../reactivity/effects.js';
import { create_block } from './utils.js';
/** /**
* @param {Comment} anchor * @param {Comment} anchor
* @param {() => boolean} get_condition * @param {() => boolean} get_condition
* @param {(anchor: Node) => import('#client').TemplateNode | import('#client').TemplateNode[]} consequent_fn * @param {(anchor: Node) => import('#client').Dom} consequent_fn
* @param {null | ((anchor: Node) => import('#client').TemplateNode | import('#client').TemplateNode[])} alternate_fn * @param {null | ((anchor: Node) => import('#client').Dom)} [alternate_fn]
* @param {boolean} [elseif] True if this is an `{:else if ...}` block rather than an `{#if ...}`, as that affects which transitions are considered 'local' * @param {boolean} [elseif] True if this is an `{:else if ...}` block rather than an `{#if ...}`, as that affects which transitions are considered 'local'
* @returns {void} * @returns {void}
*/ */
export function if_block(anchor, get_condition, consequent_fn, alternate_fn, elseif = false) { export function if_block(
const block = create_block(); anchor,
get_condition,
hydrate_block_anchor(anchor); consequent_fn,
alternate_fn = null,
/** @type {undefined | import('#client').TemplateNode | Array<import('#client').TemplateNode>} */ elseif = false
let consequent_dom; ) {
/** @type {undefined | import('#client').TemplateNode | Array<import('#client').TemplateNode>} */
let alternate_dom;
/** @type {import('#client').Effect | null} */ /** @type {import('#client').Effect | null} */
let consequent_effect = null; let consequent_effect = null;
@ -42,28 +28,21 @@ export function if_block(anchor, get_condition, consequent_fn, alternate_fn, els
/** @type {boolean | null} */ /** @type {boolean | null} */
let condition = null; let condition = null;
const if_effect = render_effect(() => { const effect = block(() => {
if (condition === (condition = !!get_condition())) return; if (condition === (condition = !!get_condition())) return;
/** 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;
if (hydrating) { if (hydrating) {
const comment_text = /** @type {Comment} */ (current_hydration_fragment?.[0])?.data; const is_else = anchor.data === HYDRATION_END_ELSE;
if ( if (condition === is_else) {
!comment_text ||
(comment_text === 'ssr:if:true' && !condition) ||
(comment_text === 'ssr:if:false' && condition)
) {
// Hydration mismatch: remove everything inside the anchor and start fresh. // Hydration mismatch: remove everything inside the anchor and start fresh.
// This could happen using when `{#if browser} .. {/if}` in SvelteKit. // This could happen with `{#if browser}...{/if}`, for example
remove(current_hydration_fragment); remove(hydrate_nodes);
set_current_hydration_fragment(null); set_hydrating(false);
mismatch = true; mismatch = true;
} else {
// Remove the ssr:if comment node or else it will confuse the subsequent hydration algorithm
current_hydration_fragment.shift();
} }
} }
@ -71,87 +50,35 @@ export function if_block(anchor, get_condition, consequent_fn, alternate_fn, els
if (consequent_effect) { if (consequent_effect) {
resume_effect(consequent_effect); resume_effect(consequent_effect);
} else { } else {
consequent_effect = render_effect( consequent_effect = branch(() => consequent_fn(anchor));
() => {
consequent_dom = consequent_fn(anchor);
return () => {
// TODO make this unnecessary by linking the dom to the effect,
// and removing automatically on teardown
if (consequent_dom !== undefined) {
remove(consequent_dom);
consequent_dom = undefined;
}
};
},
block,
true
);
} }
if (alternate_effect) { if (alternate_effect) {
pause_effect(alternate_effect, () => { pause_effect(alternate_effect, () => {
alternate_effect = null; alternate_effect = null;
if (alternate_dom) remove(alternate_dom);
}); });
} }
} else { } else {
if (alternate_effect) { if (alternate_effect) {
resume_effect(alternate_effect); resume_effect(alternate_effect);
} else if (alternate_fn) { } else if (alternate_fn) {
alternate_effect = render_effect( alternate_effect = branch(() => alternate_fn(anchor));
() => {
alternate_dom = alternate_fn(anchor);
return () => {
// TODO make this unnecessary by linking the dom to the effect,
// and removing automatically on teardown
if (alternate_dom !== undefined) {
remove(alternate_dom);
alternate_dom = undefined;
}
};
},
block,
true
);
} }
if (consequent_effect) { if (consequent_effect) {
pause_effect(consequent_effect, () => { pause_effect(consequent_effect, () => {
consequent_effect = null; consequent_effect = null;
if (consequent_dom) remove(consequent_dom);
}); });
} }
} }
if (mismatch) { if (mismatch) {
// Set fragment so that Svelte continues to operate in hydration mode // continue in hydration mode
set_current_hydration_fragment([]); set_hydrating(true);
} }
}, block); });
if (elseif) { if (elseif) {
if_effect.f |= IS_ELSEIF; effect.f |= IS_ELSEIF;
}
if_effect.ondestroy = () => {
// TODO make this unnecessary by linking the dom to the effect,
// and removing automatically on teardown
if (consequent_dom !== undefined) {
remove(consequent_dom);
}
if (alternate_dom !== undefined) {
remove(alternate_dom);
}
if (consequent_effect) {
destroy_effect(consequent_effect);
}
if (alternate_effect) {
destroy_effect(alternate_effect);
} }
};
} }

@ -1,76 +1,28 @@
import { UNINITIALIZED } from '../../constants.js'; import { UNINITIALIZED } from '../../../../constants.js';
import { hydrate_block_anchor } from '../hydration.js'; import { block, branch, pause_effect } from '../../reactivity/effects.js';
import { remove } from '../reconciler.js';
import { pause_effect, render_effect } from '../../reactivity/effects.js';
import { safe_not_equal } from '../../reactivity/equality.js'; import { safe_not_equal } from '../../reactivity/equality.js';
import { create_block } from './utils.js';
/** /**
* @template V * @template V
* @param {Comment} anchor * @param {Comment} anchor
* @param {() => V} get_key * @param {() => V} get_key
* @param {(anchor: Node) => void} render_fn * @param {(anchor: Node) => import('#client').Dom | void} render_fn
* @returns {void} * @returns {void}
*/ */
export function key_block(anchor, get_key, render_fn) { export function key_block(anchor, get_key, render_fn) {
const block = create_block();
hydrate_block_anchor(anchor);
/** @type {V | typeof UNINITIALIZED} */ /** @type {V | typeof UNINITIALIZED} */
let key = UNINITIALIZED; let key = UNINITIALIZED;
/** @type {import('#client').Effect} */ /** @type {import('#client').Effect} */
let effect; let effect;
/** block(() => {
* Every time `key` changes, we create a new effect. Old effects are
* removed from this set when they have fully transitioned out
* @type {Set<import('#client').Effect>}
*/
let effects = new Set();
const key_effect = render_effect(
() => {
if (safe_not_equal(key, (key = get_key()))) { if (safe_not_equal(key, (key = get_key()))) {
if (effect) { if (effect) {
var e = effect; pause_effect(effect);
pause_effect(e, () => {
effects.delete(e);
});
} }
effect = render_effect( effect = branch(() => render_fn(anchor));
() => {
render_fn(anchor);
const dom = block.d;
return () => {
if (dom !== null) {
remove(dom);
} }
}; });
},
block,
true,
true
);
// @ts-expect-error TODO tidy up
effect.d = block.d;
effects.add(effect);
}
},
block,
false
);
key_effect.ondestroy = () => {
for (const e of effects) {
// @ts-expect-error TODO tidy up. ondestroy should be totally unnecessary
if (e.d) remove(e.d);
}
};
} }

@ -1,30 +1,23 @@
import { render_effect } from '../../reactivity/effects.js'; import { branch, render_effect } from '../../reactivity/effects.js';
import { remove } from '../reconciler.js';
import { untrack } from '../../runtime.js';
import { create_block } from './utils.js';
/** /**
* @param {() => Function | null | undefined} get_snippet * @template {(node: import('#client').TemplateNode, ...args: any[]) => import('#client').Dom} SnippetFn
* @param {Node} node * @param {() => SnippetFn | null | undefined} get_snippet
* @param {import('#client').TemplateNode} node
* @param {(() => any)[]} args * @param {(() => any)[]} args
* @returns {void} * @returns {void}
*/ */
export function snippet(get_snippet, node, ...args) { export function snippet(get_snippet, node, ...args) {
const block = create_block(); /** @type {SnippetFn | null | undefined} */
var snippet;
render_effect(() => { render_effect(() => {
// Only rerender when the snippet function itself changes, if (snippet === (snippet = get_snippet())) return;
// not when an eagerly-read prop inside the snippet function changes
const snippet = get_snippet();
if (snippet) { if (snippet) {
untrack(() => snippet(node, ...args)); branch(() => /** @type {SnippetFn} */ (snippet)(node, ...args));
}
return () => {
if (block.d !== null) {
remove(block.d);
} }
}; });
}, block);
} }
const snippet_symbol = Symbol.for('svelte.snippet'); const snippet_symbol = Symbol.for('svelte.snippet');

@ -1,79 +1,32 @@
import { hydrate_block_anchor } from '../hydration.js'; import { block, branch, pause_effect } from '../../reactivity/effects.js';
import { pause_effect, render_effect } from '../../reactivity/effects.js';
import { remove } from '../reconciler.js';
import { create_block } from './utils.js';
// TODO this is very similar to `key`, can we deduplicate? // TODO seems weird that `anchor` is unused here — possible bug?
/** /**
* @template P * @template P
* @template {(props: P) => void} C * @template {(props: P) => void} C
* @param {Comment} anchor * @param {Comment} anchor
* @param {() => C} get_component * @param {() => C} get_component
* @param {(component: C) => void} render_fn * @param {(component: C) => import('#client').Dom | void} render_fn
* @returns {void} * @returns {void}
*/ */
export function component(anchor, get_component, render_fn) { export function component(anchor, get_component, render_fn) {
const block = create_block();
hydrate_block_anchor(anchor);
/** @type {C} */ /** @type {C} */
let component; let component;
/** @type {import('#client').Effect} */ /** @type {import('#client').Effect | null} */
let effect; let effect;
/** block(() => {
* Every time `component` changes, we create a new effect. Old effects are
* removed from this set when they have fully transitioned out
* @type {Set<import('#client').Effect>}
*/
let effects = new Set();
const component_effect = render_effect(
() => {
if (component === (component = get_component())) return; if (component === (component = get_component())) return;
if (effect) { if (effect) {
var e = effect; pause_effect(effect);
pause_effect(e, () => { effect = null;
effects.delete(e);
});
} }
if (component) { if (component) {
effect = render_effect( effect = branch(() => render_fn(component));
() => {
render_fn(component);
const dom = block.d;
return () => {
if (dom !== null) {
remove(dom);
} }
}; });
},
block,
true,
true
);
// @ts-expect-error TODO tidy up
effect.d = block.d;
effects.add(effect);
}
},
block,
false
);
component_effect.ondestroy = () => {
for (const e of effects) {
// @ts-expect-error TODO tidy up. ondestroy should be totally unnecessary
if (e.d) remove(e.d);
}
};
} }

@ -1,27 +1,28 @@
import { namespace_svg } from '../../../../constants.js'; import { namespace_svg } from '../../../../constants.js';
import { current_hydration_fragment, hydrate_block_anchor, hydrating } from '../hydration.js'; import { hydrate_anchor, hydrate_nodes, hydrating } from '../hydration.js';
import { empty } from '../operations.js'; import { empty } from '../operations.js';
import { import {
block,
branch,
destroy_effect, destroy_effect,
pause_effect, pause_effect,
render_effect, render_effect,
resume_effect resume_effect
} from '../../reactivity/effects.js'; } from '../../reactivity/effects.js';
import { remove } from '../reconciler.js';
import { is_array } from '../../utils.js'; import { is_array } from '../../utils.js';
import { set_should_intro } from '../../render.js'; import { set_should_intro } from '../../render.js';
import { current_each_item_block, set_current_each_item_block } from './each.js'; import { current_each_item, set_current_each_item } from './each.js';
import { create_block } from './utils.js'; import { current_effect } from '../../runtime.js';
import { current_block } from '../../runtime.js';
/** /**
* @param {import('#client').Block} block * @param {import('#client').Effect} effect
* @param {Element} from * @param {Element} from
* @param {Element} to * @param {Element} to
* @returns {void} * @returns {void}
*/ */
function swap_block_dom(block, from, to) { function swap_block_dom(effect, from, to) {
const dom = block.d; const dom = effect.dom;
if (is_array(dom)) { if (is_array(dom)) {
for (let i = 0; i < dom.length; i++) { for (let i = 0; i < dom.length; i++) {
if (dom[i] === from) { if (dom[i] === from) {
@ -30,7 +31,7 @@ function swap_block_dom(block, from, to) {
} }
} }
} else if (dom === from) { } else if (dom === from) {
block.d = to; effect.dom = to;
} }
} }
@ -42,11 +43,9 @@ function swap_block_dom(block, from, to) {
* @returns {void} * @returns {void}
*/ */
export function element(anchor, get_tag, is_svg, render_fn) { export function element(anchor, get_tag, is_svg, render_fn) {
const parent_block = /** @type {import('#client').Block} */ (current_block); const parent_effect = /** @type {import('#client').Effect} */ (current_effect);
const block = create_block();
hydrate_block_anchor(anchor);
render_effect(() => {
/** @type {string | null} */ /** @type {string | null} */
let tag; let tag;
@ -64,15 +63,15 @@ export function element(anchor, get_tag, is_svg, render_fn) {
* We track this so we can set it when changing the element, allowing any * We track this so we can set it when changing the element, allowing any
* `animate:` directive to bind itself to the correct block * `animate:` directive to bind itself to the correct block
*/ */
let each_item_block = current_each_item_block; let each_item_block = current_each_item;
const wrapper = render_effect(() => { block(() => {
const next_tag = get_tag() || null; const next_tag = get_tag() || null;
if (next_tag === tag) return; if (next_tag === tag) return;
// See explanation of `each_item_block` above // See explanation of `each_item_block` above
var previous_each_item_block = current_each_item_block; var previous_each_item = current_each_item;
set_current_each_item_block(each_item_block); set_current_each_item(each_item_block);
// We try our best infering the namespace in case it's not possible to determine statically, // 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, // but on the first render on the client (without hydration) the parent will be undefined,
@ -90,7 +89,7 @@ export function element(anchor, get_tag, is_svg, render_fn) {
pause_effect(effect, () => { pause_effect(effect, () => {
effect = null; effect = null;
current_tag = null; current_tag = null;
element?.remove(); // TODO this should be unnecessary element?.remove();
}); });
} else if (next_tag === current_tag) { } else if (next_tag === current_tag) {
// same tag as is currently rendered — abort outro // same tag as is currently rendered — abort outro
@ -103,56 +102,48 @@ export function element(anchor, get_tag, is_svg, render_fn) {
} }
if (next_tag && next_tag !== current_tag) { if (next_tag && next_tag !== current_tag) {
effect = render_effect( effect = branch(() => {
() => {
const prev_element = element; const prev_element = element;
element = hydrating element = hydrating
? /** @type {Element} */ (current_hydration_fragment[0]) ? /** @type {Element} */ (hydrate_nodes[0])
: ns : ns
? document.createElementNS(ns, next_tag) ? document.createElementNS(ns, next_tag)
: document.createElement(next_tag); : document.createElement(next_tag);
if (render_fn) { if (render_fn) {
let anchor; // If hydrating, use the existing ssr comment as the anchor so that the
if (hydrating) { // inner open and close methods can pick up the existing nodes correctly
// Use the existing ssr comment as the anchor so that the inner open and close var child_anchor = hydrating
// methods can pick up the existing nodes correctly ? element.firstChild && hydrate_anchor(/** @type {Comment} */ (element.firstChild))
anchor = /** @type {Comment} */ (element.firstChild); : element.appendChild(empty());
} else {
anchor = empty(); if (child_anchor) {
element.appendChild(anchor); // `child_anchor` can be undefined if this is a void element with children,
// i.e. `<svelte:element this={"hr"}>...</svelte:element>`. This is
// user error, but we warn on it elsewhere (in dev) so here we just
// silently ignore it
render_fn(element, child_anchor);
} }
render_fn(element, anchor);
} }
anchor.before(element); anchor.before(element);
if (prev_element) { if (prev_element) {
swap_block_dom(parent_block, prev_element, element); swap_block_dom(parent_effect, prev_element, element);
prev_element.remove(); prev_element.remove();
} }
}, });
block,
true
);
} }
tag = next_tag; tag = next_tag;
if (tag) current_tag = tag; if (tag) current_tag = tag;
set_should_intro(true); set_should_intro(true);
set_current_each_item_block(previous_each_item_block); set_current_each_item(previous_each_item);
}, block); });
wrapper.ondestroy = () => {
if (element !== null) {
remove(element);
block.d = null;
element = null;
}
if (effect) { return () => {
destroy_effect(effect); element?.remove();
}
}; };
});
} }

@ -1,61 +1,39 @@
import { import { hydrate_anchor, hydrate_nodes, hydrating, set_hydrate_nodes } from '../hydration.js';
current_hydration_fragment,
get_hydration_fragment,
hydrating,
set_current_hydration_fragment
} from '../hydration.js';
import { empty } from '../operations.js'; import { empty } from '../operations.js';
import { render_effect } from '../../reactivity/effects.js'; import { block } from '../../reactivity/effects.js';
import { remove } from '../reconciler.js'; import { HYDRATION_START } from '../../../../constants.js';
import { create_block } from './utils.js';
/** /**
* @param {(anchor: Node | null) => void} render_fn * @param {(anchor: Node) => import('#client').Dom | void} render_fn
* @returns {void} * @returns {void}
*/ */
export function head(render_fn) { export function head(render_fn) {
const block = create_block();
// The head function may be called after the first hydration pass and ssr comment nodes may still be present, // 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. // therefore we need to skip that when we detect that we're not in hydration mode.
let hydration_fragment = null; let previous_hydrate_nodes = null;
let previous_hydration_fragment = null; let was_hydrating = hydrating;
let is_hydrating = hydrating; /** @type {Comment | Text} */
if (is_hydrating) { var anchor;
hydration_fragment = get_hydration_fragment(document.head.firstChild);
previous_hydration_fragment = current_hydration_fragment;
set_current_hydration_fragment(hydration_fragment);
}
try { if (hydrating) {
const head_effect = render_effect( previous_hydrate_nodes = hydrate_nodes;
() => {
const current = block.d; let anchor = /** @type {import('#client').TemplateNode} */ (document.head.firstChild);
if (current !== null) { while (anchor.nodeType !== 8 || /** @type {Comment} */ (anchor).data !== HYDRATION_START) {
remove(current); anchor = /** @type {import('#client').TemplateNode} */ (anchor.nextSibling);
block.d = null;
}
let anchor = null;
if (!hydrating) {
anchor = empty();
document.head.appendChild(anchor);
} }
render_fn(anchor);
},
block,
false
);
head_effect.ondestroy = () => { anchor = /** @type {import('#client').TemplateNode} */ (hydrate_anchor(anchor));
const current = block.d; } else {
if (current !== null) { anchor = document.head.appendChild(empty());
remove(current);
} }
};
try {
block(() => render_fn(anchor));
} finally { } finally {
if (is_hydrating) { if (was_hydrating) {
set_current_hydration_fragment(previous_hydration_fragment); set_hydrate_nodes(/** @type {import('#client').TemplateNode[]} */ (previous_hydrate_nodes));
} }
} }
} }

@ -1,7 +0,0 @@
/** @returns {import('#client').Block} */
export function create_block() {
return {
// dom
d: null
};
}

@ -1,53 +1,38 @@
import { effect } from '../../reactivity/effects.js'; import { effect, render_effect } from '../../reactivity/effects.js';
import { deep_read_state, untrack } from '../../runtime.js'; import { deep_read_state, untrack } from '../../runtime.js';
/** /**
* @template P * @template P
* @param {Element} dom * @param {Element} dom
* @param {(dom: Element, value?: P) => import('#client').ActionPayload<P>} action * @param {(dom: Element, value?: P) => import('#client').ActionPayload<P>} action
* @param {() => P} [value_fn] * @param {() => P} [get_value]
* @returns {void} * @returns {void}
*/ */
export function action(dom, action, value_fn) { export function action(dom, action, get_value) {
/** @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(() => { effect(() => {
if (value_fn) { var payload = untrack(() => action(dom, get_value?.()) || {});
var value = value_fn();
untrack(() => { if (get_value && payload?.update) {
if (payload === undefined) { var inited = false;
payload = action(dom, value) || {};
needs_deep_read = !!payload?.update; render_effect(() => {
} else { var value = get_value();
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. // 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 // 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. // together with actions and mutation, it wouldn't notice the change without a deep read.
if (needs_deep_read) {
deep_read_state(value); deep_read_state(value);
}
} else { if (inited) {
untrack(() => (payload = action(dom))); /** @type {Function} */ (payload.update)(value);
} }
}); });
effect(() => { inited = true;
if (payload !== undefined) {
var destroy = payload.destroy;
if (typeof destroy === 'function') {
return () => {
/** @type {Function} */ (destroy)();
};
} }
if (payload?.destroy) {
return () => /** @type {Function} */ (payload.destroy)();
} }
}); });
} }

@ -1,6 +1,5 @@
import { DEV } from 'esm-env'; import { DEV } from 'esm-env';
import { hydrating } from '../hydration.js'; 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 { get_descriptors, map_get, map_set, object_assign } from '../../utils.js';
import { AttributeAliases, DelegatedEvents, namespace_svg } from '../../../../constants.js'; import { AttributeAliases, DelegatedEvents, namespace_svg } from '../../../../constants.js';
import { delegate } from './events.js'; import { delegate } from './events.js';
@ -14,62 +13,43 @@ import { autofocus } from './misc.js';
*/ */
export function remove_input_attr_defaults(dom) { export function remove_input_attr_defaults(dom) {
if (hydrating) { if (hydrating) {
attr(dom, 'value', null); set_attribute(dom, 'value', null);
attr(dom, 'checked', null); set_attribute(dom, 'checked', null);
} }
} }
/** /**
* @param {Element} dom * @param {Element} element
* @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} attribute
* @param {string | null} value * @param {string | null} value
*/ */
export function attr(dom, attribute, value) { export function set_attribute(element, attribute, value) {
value = value == null ? null : value + ''; value = value == null ? null : value + '';
if (DEV) { // @ts-expect-error
check_src_in_dev_hydration(dom, attribute, value); var attributes = (element.__attributes ??= {});
}
if ( if (hydrating) {
!hydrating || attributes[attribute] = element.getAttribute(attribute);
(dom.getAttribute(attribute) !== value &&
// If we reset those, they would result in another network request, which we want to avoid. if (attribute === 'src' || attribute === 'href' || attribute === 'srcset') {
check_src_in_dev_hydration(element, attribute, value);
// If we reset these attributes, 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 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 // (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) // same url, so we would need to create hidden anchor elements to compare them)
attribute !== 'src' && return;
attribute !== 'href' &&
attribute !== 'srcset')
) {
if (value === null) {
dom.removeAttribute(attribute);
} else {
dom.setAttribute(attribute, value);
} }
} }
}
/** if (attributes[attribute] === (attributes[attribute] = value)) return;
* @param {Element} dom
* @param {string} attribute if (value === null) {
* @param {() => string} value element.removeAttribute(attribute);
*/ } else {
export function xlink_attr_effect(dom, attribute, value) { element.setAttribute(attribute, value);
render_effect(() => { }
xlink_attr(dom, attribute, value());
});
} }
/** /**
@ -77,21 +57,10 @@ export function xlink_attr_effect(dom, attribute, value) {
* @param {string} attribute * @param {string} attribute
* @param {string} value * @param {string} value
*/ */
export function xlink_attr(dom, attribute, value) { export function set_xlink_attribute(dom, attribute, value) {
dom.setAttributeNS('http://www.w3.org/1999/xlink', 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 {any} node
* @param {string} prop * @param {string} prop
@ -99,38 +68,26 @@ export function set_custom_element_data_effect(node, prop, value) {
*/ */
export function set_custom_element_data(node, prop, value) { export function set_custom_element_data(node, prop, value) {
if (prop in node) { if (prop in node) {
node[prop] = typeof node[prop] === 'boolean' && value === '' ? true : value; var curr_val = node[prop];
var next_val = typeof curr_val === 'boolean' && value === '' ? true : value;
if (typeof curr_val !== 'object' || curr_val !== next_val) {
node[prop] = next_val;
}
} else { } else {
attr(node, prop, value); set_attribute(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 * Spreads attributes onto a DOM element, taking into account the currently set attributes
* @param {Element & ElementCSSInlineStyle} dom * @param {Element & ElementCSSInlineStyle} element
* @param {Record<string, unknown> | undefined} prev * @param {Record<string, unknown> | undefined} prev
* @param {Record<string, unknown>[]} attrs * @param {Record<string, unknown>[]} attrs
* @param {boolean} lowercase_attributes * @param {boolean} lowercase_attributes
* @param {string} css_hash * @param {string} css_hash
* @returns {Record<string, unknown>} * @returns {Record<string, unknown>}
*/ */
export function spread_attributes(dom, prev, attrs, lowercase_attributes, css_hash) { export function set_attributes(element, prev, attrs, lowercase_attributes, css_hash) {
var next = object_assign({}, ...attrs); var next = object_assign({}, ...attrs);
var has_hash = css_hash.length !== 0; var has_hash = css_hash.length !== 0;
@ -144,8 +101,8 @@ export function spread_attributes(dom, prev, attrs, lowercase_attributes, css_ha
next.class = ''; next.class = '';
} }
var setters = map_get(setters_cache, dom.nodeName); var setters = map_get(setters_cache, element.nodeName);
if (!setters) map_set(setters_cache, dom.nodeName, (setters = get_setters(dom))); if (!setters) map_set(setters_cache, element.nodeName, (setters = get_setters(element)));
for (key in next) { for (key in next) {
var value = next[key]; var value = next[key];
@ -170,27 +127,27 @@ export function spread_attributes(dom, prev, attrs, lowercase_attributes, css_ha
} }
if (!delegated && prev?.[key]) { if (!delegated && prev?.[key]) {
dom.removeEventListener(event_name, /** @type {any} */ (prev[key]), opts); element.removeEventListener(event_name, /** @type {any} */ (prev[key]), opts);
} }
if (value != null) { if (value != null) {
if (!delegated) { if (!delegated) {
dom.addEventListener(event_name, value, opts); element.addEventListener(event_name, value, opts);
} else { } else {
// @ts-ignore // @ts-ignore
dom[`__${event_name}`] = value; element[`__${event_name}`] = value;
delegate([event_name]); delegate([event_name]);
} }
} }
} else if (value == null) { } else if (value == null) {
dom.removeAttribute(key); element.removeAttribute(key);
} else if (key === 'style') { } else if (key === 'style') {
dom.style.cssText = value + ''; element.style.cssText = value + '';
} else if (key === 'autofocus') { } else if (key === 'autofocus') {
autofocus(/** @type {HTMLElement} */ (dom), Boolean(value)); autofocus(/** @type {HTMLElement} */ (element), Boolean(value));
} else if (key === '__value' || key === 'value') { } else if (key === '__value' || key === 'value') {
// @ts-ignore // @ts-ignore
dom.value = dom[key] = dom.__value = value; element.value = element[key] = element.__value = value;
} else { } else {
var name = key; var name = key;
if (lowercase_attributes) { if (lowercase_attributes) {
@ -199,17 +156,11 @@ export function spread_attributes(dom, prev, attrs, lowercase_attributes, css_ha
} }
if (setters.includes(name)) { if (setters.includes(name)) {
if (DEV) { if (hydrating && (name === 'src' || name === 'href' || name === 'srcset')) {
check_src_in_dev_hydration(dom, name, value); check_src_in_dev_hydration(element, name, value);
} } else {
if (
!hydrating ||
// @ts-ignore see attr method for an explanation of src/srcset
(dom[name] !== value && name !== 'src' && name !== 'href' && name !== 'srcset')
) {
// @ts-ignore // @ts-ignore
dom[name] = value; element[name] = value;
} }
} else if (typeof value !== 'function') { } else if (typeof value !== 'function') {
if (has_hash && name === 'class') { if (has_hash && name === 'class') {
@ -217,7 +168,7 @@ export function spread_attributes(dom, prev, attrs, lowercase_attributes, css_ha
value += css_hash; value += css_hash;
} }
attr(dom, name, value); set_attribute(element, name, value);
} }
} }
} }
@ -225,27 +176,13 @@ export function spread_attributes(dom, prev, attrs, lowercase_attributes, css_ha
return next; 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 {Element} node
* @param {Record<string, unknown> | undefined} prev * @param {Record<string, unknown> | undefined} prev
* @param {Record<string, unknown>[]} attrs * @param {Record<string, unknown>[]} attrs
* @param {string} css_hash * @param {string} css_hash
*/ */
export function spread_dynamic_element_attributes(node, prev, attrs, css_hash) { export function set_dynamic_element_attributes(node, prev, attrs, css_hash) {
if (node.tagName.includes('-')) { if (node.tagName.includes('-')) {
var next = object_assign({}, ...attrs); var next = object_assign({}, ...attrs);
@ -260,15 +197,15 @@ export function spread_dynamic_element_attributes(node, prev, attrs, css_hash) {
} }
return next; return next;
} else { }
return spread_attributes(
return set_attributes(
/** @type {Element & ElementCSSInlineStyle} */ (node), /** @type {Element & ElementCSSInlineStyle} */ (node),
prev, prev,
attrs, attrs,
node.namespaceURI !== namespace_svg, node.namespaceURI !== namespace_svg,
css_hash css_hash
); );
}
} }
/** /**
@ -301,22 +238,20 @@ function get_setters(element) {
} }
/** /**
* @param {any} dom * @param {any} element
* @param {string} attribute * @param {string} attribute
* @param {string | null} value * @param {string | null} value
*/ */
function check_src_in_dev_hydration(dom, attribute, value) { function check_src_in_dev_hydration(element, attribute, value) {
if (!hydrating) return; if (!DEV) return;
if (attribute !== 'src' && attribute !== 'href' && attribute !== 'srcset') return; if (attribute === 'srcset' && srcset_url_equal(element, value)) return;
if (src_url_equal(element.getAttribute(attribute) ?? '', value ?? '')) 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 // eslint-disable-next-line no-console
console.error( console.error(
`Detected a ${attribute} attribute value change during hydration. This will not be repaired during hydration, ` + `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:`, `the ${attribute} value that came from the server will be used. Related element:`,
dom, element,
' Differing value:', ' Differing value:',
value value
); );
@ -342,7 +277,7 @@ function split_srcset(srcset) {
* @param {string | undefined | null} srcset * @param {string | undefined | null} srcset
* @returns {boolean} * @returns {boolean}
*/ */
export function srcset_url_equal(element, srcset) { function srcset_url_equal(element, srcset) {
var element_urls = split_srcset(element.srcset); var element_urls = split_srcset(element.srcset);
var urls = split_srcset(srcset ?? ''); var urls = split_srcset(srcset ?? '');

@ -1,6 +1,7 @@
import { DEV } from 'esm-env'; import { DEV } from 'esm-env';
import { render_effect, user_effect } from '../../../reactivity/effects.js'; import { render_effect, effect } from '../../../reactivity/effects.js';
import { stringify } from '../../../render.js'; import { stringify } from '../../../render.js';
import { listen_to_event_and_reset_event } from './shared.js';
/** /**
* @param {HTMLInputElement} input * @param {HTMLInputElement} input
@ -9,7 +10,7 @@ import { stringify } from '../../../render.js';
* @returns {void} * @returns {void}
*/ */
export function bind_value(input, get_value, update) { export function bind_value(input, get_value, update) {
input.addEventListener('input', () => { listen_to_event_and_reset_event(input, 'input', () => {
if (DEV && input.type === 'checkbox') { if (DEV && input.type === 'checkbox') {
throw new Error( throw new Error(
'Using bind:value together with a checkbox input is not allowed. Use bind:checked instead' 'Using bind:value together with a checkbox input is not allowed. Use bind:checked instead'
@ -72,7 +73,10 @@ export function bind_group(inputs, group_index, input, get_value, update) {
binding_group.push(input); binding_group.push(input);
input.addEventListener('change', () => { listen_to_event_and_reset_event(
input,
'change',
() => {
// @ts-ignore // @ts-ignore
var value = input.__value; var value = input.__value;
@ -81,7 +85,10 @@ export function bind_group(inputs, group_index, input, get_value, update) {
} }
update(value); update(value);
}); },
// TODO better default value handling
() => update(is_checkbox ? [] : null)
);
render_effect(() => { render_effect(() => {
var value = get_value(); var value = get_value();
@ -96,11 +103,6 @@ export function bind_group(inputs, group_index, input, get_value, update) {
} }
}); });
user_effect(() => {
// necessary to maintain binding group order in all insertion scenarios. TODO optimise
binding_group.sort((a, b) => (a.compareDocumentPosition(b) === 4 ? -1 : 1));
});
render_effect(() => { render_effect(() => {
return () => { return () => {
var index = binding_group.indexOf(input); var index = binding_group.indexOf(input);
@ -110,6 +112,11 @@ export function bind_group(inputs, group_index, input, get_value, update) {
} }
}; };
}); });
effect(() => {
// necessary to maintain binding group order in all insertion scenarios. TODO optimise
binding_group.sort((a, b) => (a.compareDocumentPosition(b) === 4 ? -1 : 1));
});
} }
/** /**
@ -119,7 +126,7 @@ export function bind_group(inputs, group_index, input, get_value, update) {
* @returns {void} * @returns {void}
*/ */
export function bind_checked(input, get_value, update) { export function bind_checked(input, get_value, update) {
input.addEventListener('change', () => { listen_to_event_and_reset_event(input, 'change', () => {
var value = input.checked; var value = input.checked;
update(value); update(value);
}); });
@ -173,3 +180,17 @@ function is_numberlike_input(input) {
function to_number(value) { function to_number(value) {
return value === '' ? null : +value; return value === '' ? null : +value;
} }
/**
* @param {HTMLInputElement} input
* @param {() => FileList | null} get_value
* @param {(value: FileList | null) => void} update
*/
export function bind_files(input, get_value, update) {
listen_to_event_and_reset_event(input, 'change', () => {
update(input.files);
});
render_effect(() => {
input.files = get_value();
});
}

@ -1,5 +1,5 @@
import { hydrating } from '../../hydration.js'; import { hydrating } from '../../hydration.js';
import { destroy_effect, managed_effect, render_effect } from '../../../reactivity/effects.js'; import { render_effect, effect } from '../../../reactivity/effects.js';
import { listen } from './shared.js'; import { listen } from './shared.js';
/** @param {TimeRanges} ranges */ /** @param {TimeRanges} ranges */
@ -115,32 +115,9 @@ export function bind_ready_state(media, update) {
export function bind_playback_rate(media, get_value, update) { export function bind_playback_rate(media, get_value, update) {
var updating = false; 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. // 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. // For hydration we could do it immediately but the additional code is not worth the lost microtask.
effect(() => {
/** @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(); var value = get_value();
// through isNaN we also allow number strings, which is more robust // through isNaN we also allow number strings, which is more robust
@ -148,14 +125,11 @@ export function bind_playback_rate(media, get_value, update) {
updating = true; updating = true;
media.playbackRate = /** @type {number} */ (value); media.playbackRate = /** @type {number} */ (value);
} }
});
});
render_effect(() => () => { listen(media, ['ratechange'], () => {
destroyed = true; if (!updating) update(media.playbackRate);
if (render) { updating = false;
destroy_effect(render); });
}
}); });
} }

@ -1,4 +1,5 @@
import { effect } from '../../../reactivity/effects.js'; import { effect } from '../../../reactivity/effects.js';
import { listen_to_event_and_reset_event } from './shared.js';
import { untrack } from '../../../runtime.js'; import { untrack } from '../../../runtime.js';
/** /**
@ -76,7 +77,7 @@ export function init_select(select, get_value) {
export function bind_select_value(select, get_value, update) { export function bind_select_value(select, get_value, update) {
var mounting = true; var mounting = true;
select.addEventListener('change', () => { listen_to_event_and_reset_event(select, 'change', () => {
/** @type {unknown} */ /** @type {unknown} */
var value; var value;

@ -25,3 +25,51 @@ export function listen(target, events, handler, call_handler_immediately = true)
}; };
}); });
} }
let listening_to_form_reset = false;
/**
* Listen to the given event, and then instantiate a global form reset listener if not already done,
* to notify all bindings when the form is reset
* @param {HTMLElement} element
* @param {string} event
* @param {() => void} handler
* @param {() => void} [on_reset]
*/
export function listen_to_event_and_reset_event(element, event, handler, on_reset = handler) {
element.addEventListener(event, handler);
// @ts-expect-error
const prev = element.__on_r;
if (prev) {
// special case for checkbox that can have multiple binds (group & checked)
// @ts-expect-error
element.__on_r = () => {
prev();
on_reset();
};
} else {
// @ts-expect-error
element.__on_r = on_reset;
}
if (!listening_to_form_reset) {
listening_to_form_reset = true;
document.addEventListener(
'reset',
(evt) => {
// Needs to happen one tick later or else the dom properties of the form
// elements have not updated to their reset values yet
Promise.resolve().then(() => {
if (!evt.defaultPrevented) {
for (const e of /**@type {HTMLFormElement} */ (evt.target).elements) {
// @ts-expect-error
e.__on_r?.();
}
}
});
},
// In the capture phase to guarantee we get noticed of it (no possiblity of stopPropagation)
{ capture: true }
);
}
}

@ -1,5 +1,5 @@
import { STATE_SYMBOL } from '../../../constants.js'; import { STATE_SYMBOL } from '../../../constants.js';
import { effect } from '../../../reactivity/effects.js'; import { effect, render_effect } from '../../../reactivity/effects.js';
import { untrack } from '../../../runtime.js'; import { untrack } from '../../../runtime.js';
/** /**
@ -22,13 +22,14 @@ function is_bound_this(bound_value, element_or_component) {
* @returns {void} * @returns {void}
*/ */
export function bind_this(element_or_component, update, get_value, get_parts) { export function bind_this(element_or_component, update, get_value, get_parts) {
effect(() => {
/** @type {unknown[]} */ /** @type {unknown[]} */
var old_parts; var old_parts;
/** @type {unknown[]} */ /** @type {unknown[]} */
var parts; var parts;
var e = effect(() => { render_effect(() => {
old_parts = parts; old_parts = parts;
// We only track changes to the parts, not the value itself to avoid unnecessary reruns. // We only track changes to the parts, not the value itself to avoid unnecessary reruns.
parts = get_parts?.() || []; parts = get_parts?.() || [];
@ -45,16 +46,12 @@ export function bind_this(element_or_component, update, get_value, get_parts) {
}); });
}); });
// Add effect teardown (likely causes: if block became false, each item removed, component unmounted). return () => {
// 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(() => { effect(() => {
if (parts && is_bound_this(get_value(...parts), element_or_component)) { if (parts && is_bound_this(get_value(...parts), element_or_component)) {
update(null, ...parts); update(null, ...parts);
} }
}); });
}; };
});
} }

@ -1,35 +1,12 @@
import { hydrating } from '../hydration.js'; import { hydrating } from '../hydration.js';
import { set_class_name } from '../operations.js'; import { set_class_name } from '../operations.js';
import { render_effect } from '../../reactivity/effects.js';
/**
* @param {HTMLElement} dom
* @param {() => string} value
* @returns {void}
*/
export function class_name_effect(dom, value) {
render_effect(() => {
class_name(dom, value());
});
}
/**
* @param {SVGElement} dom
* @param {() => string} value
* @returns {void}
*/
export function svg_class_name_effect(dom, value) {
render_effect(() => {
svg_class_name(dom, value());
});
}
/** /**
* @param {SVGElement} dom * @param {SVGElement} dom
* @param {string} value * @param {string} value
* @returns {void} * @returns {void}
*/ */
export function svg_class_name(dom, value) { export function set_svg_class(dom, value) {
// @ts-expect-error need to add __className to patched prototype // @ts-expect-error need to add __className to patched prototype
var prev_class_name = dom.__className; var prev_class_name = dom.__className;
var next_class_name = to_class(value); var next_class_name = to_class(value);
@ -58,7 +35,7 @@ export function svg_class_name(dom, value) {
* @param {string} value * @param {string} value
* @returns {void} * @returns {void}
*/ */
export function class_name(dom, value) { export function set_class(dom, value) {
// @ts-expect-error need to add __className to patched prototype // @ts-expect-error need to add __className to patched prototype
var prev_class_name = dom.__className; var prev_class_name = dom.__className;
var next_class_name = to_class(value); var next_class_name = to_class(value);
@ -90,7 +67,7 @@ export function class_name(dom, value) {
* @param {V} value * @param {V} value
* @returns {string | V} * @returns {string | V}
*/ */
export function to_class(value) { function to_class(value) {
return value == null ? '' : value; return value == null ? '' : value;
} }
@ -100,22 +77,10 @@ export function to_class(value) {
* @param {boolean} value * @param {boolean} value
* @returns {void} * @returns {void}
*/ */
export function class_toggle(dom, class_name, value) { export function toggle_class(dom, class_name, value) {
if (value) { if (value) {
dom.classList.add(class_name); dom.classList.add(class_name);
} else { } else {
dom.classList.remove(class_name); 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,8 @@
import { createClassComponent } from '../../../../legacy/legacy-client.js'; import { createClassComponent } from '../../../../legacy/legacy-client.js';
import { destroy_effect, render_effect } from '../../reactivity/effects.js'; import { destroy_effect, render_effect } from '../../reactivity/effects.js';
import { open, close } from '../template.js';
import { define_property } from '../../utils.js';
import { add_snippet_symbol } from '../blocks/snippet.js'; import { add_snippet_symbol } from '../blocks/snippet.js';
import { append } from '../template.js';
import { define_property, object_keys } from '../../utils.js';
/** /**
* @typedef {Object} CustomElementPropDefinition * @typedef {Object} CustomElementPropDefinition
@ -99,14 +99,10 @@ if (typeof HTMLElement === 'function') {
* @param {Element} anchor * @param {Element} anchor
*/ */
return (anchor) => { return (anchor) => {
const node = open(anchor, true, () => {
const slot = document.createElement('slot'); const slot = document.createElement('slot');
if (name !== 'default') { if (name !== 'default') slot.name = name;
slot.name = name;
} append(anchor, slot);
return slot;
});
close(anchor, /** @type {Element} */ (node));
}; };
} }
/** @type {Record<string, any>} */ /** @type {Record<string, any>} */
@ -150,7 +146,7 @@ if (typeof HTMLElement === 'function') {
// Reflect component props as attributes // Reflect component props as attributes
this.$$me = render_effect(() => { this.$$me = render_effect(() => {
this.$$r = true; this.$$r = true;
for (const key of Object.keys(this.$$c)) { for (const key of object_keys(this.$$c)) {
if (!this.$$p_d[key]?.reflect) continue; if (!this.$$p_d[key]?.reflect) continue;
this.$$d[key] = this.$$c[key]; this.$$d[key] = this.$$c[key];
const attribute_value = get_custom_element_value( const attribute_value = get_custom_element_value(
@ -210,7 +206,7 @@ if (typeof HTMLElement === 'function') {
*/ */
$$g_p(attribute_name) { $$g_p(attribute_name) {
return ( return (
Object.keys(this.$$p_d).find( object_keys(this.$$p_d).find(
(key) => (key) =>
this.$$p_d[key].attribute === attribute_name || this.$$p_d[key].attribute === attribute_name ||
(!this.$$p_d[key].attribute && key.toLowerCase() === attribute_name) (!this.$$p_d[key].attribute && key.toLowerCase() === attribute_name)
@ -295,12 +291,12 @@ export function create_custom_element(
this.$$p_d = props_definition; this.$$p_d = props_definition;
} }
static get observedAttributes() { static get observedAttributes() {
return Object.keys(props_definition).map((key) => return object_keys(props_definition).map((key) =>
(props_definition[key].attribute || key).toLowerCase() (props_definition[key].attribute || key).toLowerCase()
); );
} }
}; };
Object.keys(props_definition).forEach((prop) => { object_keys(props_definition).forEach((prop) => {
define_property(Class.prototype, prop, { define_property(Class.prototype, prop, {
get() { get() {
return this.$$c && prop in this.$$c ? this.$$c[prop] : this.$$d[prop]; return this.$$c && prop in this.$$c ? this.$$c[prop] : this.$$d[prop];

@ -1,6 +1,5 @@
import { hydrating } from '../hydration.js'; import { hydrating } from '../hydration.js';
import { render_effect } from '../../reactivity/effects.js'; import { effect } from '../../reactivity/effects.js';
import { current_block } from '../../runtime.js';
/** /**
* @param {HTMLElement} dom * @param {HTMLElement} dom
@ -11,16 +10,12 @@ export function autofocus(dom, value) {
if (value) { if (value) {
const body = document.body; const body = document.body;
dom.autofocus = true; dom.autofocus = true;
render_effect(
() => { effect(() => {
if (document.activeElement === body) { if (document.activeElement === body) {
dom.focus(); dom.focus();
} }
}, });
current_block,
true,
false
);
} }
} }

@ -1,12 +1,10 @@
import { render_effect } from '../../reactivity/effects.js';
/** /**
* @param {HTMLElement} dom * @param {HTMLElement} dom
* @param {string} key * @param {string} key
* @param {string} value * @param {string} value
* @param {boolean} [important] * @param {boolean} [important]
*/ */
export function style(dom, key, value, important) { export function set_style(dom, key, value, important) {
const style = dom.style; const style = dom.style;
const prev_value = style.getPropertyValue(key); const prev_value = style.getPropertyValue(key);
if (value == null) { if (value == null) {
@ -17,17 +15,3 @@ export function style(dom, key, value, important) {
style.setProperty(key, value, important ? 'important' : ''); 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,11 +1,11 @@
import { noop } from '../../../common.js'; import { noop } from '../../../shared/utils.js';
import { user_effect } from '../../reactivity/effects.js'; import { effect } from '../../reactivity/effects.js';
import { current_effect, untrack } from '../../runtime.js'; import { current_effect, untrack } from '../../runtime.js';
import { raf } from '../../timing.js'; import { raf } from '../../timing.js';
import { loop } from '../../loop.js'; import { loop } from '../../loop.js';
import { should_intro } from '../../render.js'; import { should_intro } from '../../render.js';
import { is_function } from '../../utils.js'; import { is_function } from '../../utils.js';
import { current_each_item_block } from '../blocks/each.js'; import { current_each_item } from '../blocks/each.js';
import { TRANSITION_GLOBAL, TRANSITION_IN, TRANSITION_OUT } from '../../../../constants.js'; import { TRANSITION_GLOBAL, TRANSITION_IN, TRANSITION_OUT } from '../../../../constants.js';
import { EFFECT_RAN } from '../../constants.js'; import { EFFECT_RAN } from '../../constants.js';
@ -77,7 +77,7 @@ const linear = (t) => t;
* @param {(() => P) | null} get_params * @param {(() => P) | null} get_params
*/ */
export function animation(element, get_fn, get_params) { export function animation(element, get_fn, get_params) {
var block = /** @type {import('#client').EachItem} */ (current_each_item_block); var item = /** @type {import('#client').EachItem} */ (current_each_item);
/** @type {DOMRect} */ /** @type {DOMRect} */
var from; var from;
@ -88,7 +88,7 @@ export function animation(element, get_fn, get_params) {
/** @type {import('#client').Animation | undefined} */ /** @type {import('#client').Animation | undefined} */
var animation; var animation;
block.a ??= { item.a ??= {
element, element,
measure() { measure() {
from = this.element.getBoundingClientRect(); from = this.element.getBoundingClientRect();
@ -118,7 +118,7 @@ export function animation(element, get_fn, get_params) {
// when an animation manager already exists, if the tag changes. in that case, we need to // when an animation manager already exists, if the tag changes. in that case, we need to
// swap out the element rather than creating a new manager, in case it happened at the same // swap out the element rather than creating a new manager, in case it happened at the same
// moment as a reconciliation // moment as a reconciliation
block.a.element = element; item.a.element = element;
} }
/** /**
@ -202,18 +202,18 @@ export function transition(flags, element, get_fn, get_params) {
} }
}; };
var effect = /** @type {import('#client').Effect} */ (current_effect); var e = /** @type {import('#client').Effect} */ (current_effect);
(effect.transitions ??= []).push(transition); (e.transitions ??= []).push(transition);
// if this is a local transition, we only want to run it if the parent (block) effect's // if this is a local transition, we only want to run it if the parent (block) effect's
// parent (branch) effect is where the state change happened. we can determine that by // parent (branch) effect is where the state change happened. we can determine that by
// looking at whether the branch effect is currently initializing // looking at whether the branch effect is currently initializing
if (is_intro && should_intro) { if (is_intro && should_intro) {
var parent = /** @type {import('#client').Effect} */ (effect.parent); var parent = /** @type {import('#client').Effect} */ (e.parent);
if (is_global || (parent.f & EFFECT_RAN) !== 0) { if (is_global || (parent.f & EFFECT_RAN) !== 0) {
user_effect(() => { effect(() => {
untrack(() => transition.in()); untrack(() => transition.in());
}); });
} }
@ -237,7 +237,7 @@ function animate(element, options, counterpart, t2, callback) {
/** @type {import('#client').Animation} */ /** @type {import('#client').Animation} */
var a; var a;
user_effect(() => { effect(() => {
var o = untrack(() => options({ direction: t2 === 1 ? 'in' : 'out' })); var o = untrack(() => options({ direction: t2 === 1 ? 'in' : 'out' }));
a = animate(element, o, counterpart, t2, callback); a = animate(element, o, counterpart, t2, callback);
}); });
@ -301,7 +301,17 @@ function animate(element, options, counterpart, t2, callback) {
.then(() => { .then(() => {
callback?.(); callback?.();
}) })
.catch(noop); .catch((e) => {
// Error for DOMException: The user aborted a request. This results in two things:
// - startTime is `null`
// - currentTime is `null`
// We can't use the existence of an AbortError as this error and error code is shared
// with other Web APIs such as fetch().
if (animation.startTime !== null && animation.currentTime !== null) {
throw e;
}
});
} else { } else {
// Timer // Timer
if (t1 === 0) { if (t1 === 0) {

@ -1,7 +1,4 @@
// Handle hydration import { HYDRATION_END, HYDRATION_START } from '../../../constants.js';
import { schedule_task } from './task.js';
import { empty } from './operations.js';
/** /**
* Use this variable to guard everything related to hydration code so it can be treeshaken out * Use this variable to guard everything related to hydration code so it can be treeshaken out
@ -9,90 +6,65 @@ import { empty } from './operations.js';
*/ */
export let hydrating = false; export let hydrating = false;
/** @param {boolean} value */
export function set_hydrating(value) {
hydrating = value;
}
/** /**
* Array of nodes to traverse for hydration. This will be null if we're not hydrating, but for * Array of nodes to traverse for hydration. This will be null if we're not hydrating, but for
* the sake of simplicity we're not going to use `null` checks everywhere and instead rely on * the sake of simplicity we're not going to use `null` checks everywhere and instead rely on
* the `hydrating` flag to tell whether or not we're in hydration mode at which point this is set. * the `hydrating` flag to tell whether or not we're in hydration mode at which point this is set.
* @type {import('../types.js').TemplateNode[]} * @type {import('#client').TemplateNode[]}
*/ */
export let current_hydration_fragment = /** @type {any} */ (null); export let hydrate_nodes = /** @type {any} */ (null);
/** /** @param {import('#client').TemplateNode[]} nodes */
* @param {null | import('../types.js').TemplateNode[]} fragment export function set_hydrate_nodes(nodes) {
* @returns {void} hydrate_nodes = nodes;
*/
export function set_current_hydration_fragment(fragment) {
hydrating = fragment !== null;
current_hydration_fragment = /** @type {import('../types.js').TemplateNode[]} */ (fragment);
} }
/** /**
* Returns all nodes between the first `<!--ssr:...-->` comment tag pair encountered. * This function is only called when `hydrating` is true. If passed a `<!--[-->` opening
* @param {Node | null} node * hydration marker, it finds the corresponding closing marker and sets `hydrate_nodes`
* @param {boolean} [insert_text] Whether to insert an empty text node if the fragment is empty * to everything between the markers, before returning the closing marker.
* @returns {import('../types.js').TemplateNode[] | null} * @param {Node} node
* @returns {Node}
*/ */
export function get_hydration_fragment(node, insert_text = false) { export function hydrate_anchor(node) {
/** @type {import('../types.js').TemplateNode[]} */ if (node.nodeType !== 8) {
const fragment = []; return node;
}
/** @type {null | Node} */ var current = /** @type {Node | null} */ (node);
let current_node = node;
/** @type {null | string} */ // TODO this could have false positives, if a user comment consisted of `[`. need to tighten that up
let target_depth = null; if (/** @type {Comment} */ (current)?.data !== HYDRATION_START) {
while (current_node !== null) { return node;
const node_type = current_node.nodeType;
const next_sibling = current_node.nextSibling;
if (node_type === 8) {
const data = /** @type {Comment} */ (current_node).data;
if (data.startsWith('ssr:')) {
const depth = data.slice(4);
if (target_depth === null) {
target_depth = depth;
} else if (depth === target_depth) {
if (insert_text && fragment.length === 0) {
const text = empty();
fragment.push(text);
/** @type {Node} */ (current_node.parentNode).insertBefore(text, current_node);
}
return fragment;
} else {
fragment.push(/** @type {Text | Comment | Element} */ (current_node));
}
current_node = next_sibling;
continue;
} }
/** @type {Node[]} */
var nodes = [];
var depth = 0;
while ((current = /** @type {Node} */ (current).nextSibling) !== null) {
if (current.nodeType === 8) {
var data = /** @type {Comment} */ (current).data;
if (data === HYDRATION_START) {
depth += 1;
} else if (data[0] === HYDRATION_END) {
if (depth === 0) {
hydrate_nodes = /** @type {import('#client').TemplateNode[]} */ (nodes);
return current;
} }
if (target_depth !== null) {
fragment.push(/** @type {Text | Comment | Element} */ (current_node)); depth -= 1;
} }
current_node = next_sibling;
} }
return null;
}
/**
* @param {Node} node
* @returns {void}
*/
export function hydrate_block_anchor(node) {
if (!hydrating) return;
if (node.nodeType === 8) { nodes.push(current);
// @ts-ignore
let fragment = node.$$fragment;
if (fragment === undefined) {
fragment = get_hydration_fragment(node);
} else {
schedule_task(() => {
// @ts-expect-error clean up memory
node.$$fragment = undefined;
});
}
set_current_hydration_fragment(fragment);
} else {
const first_child = /** @type {Element | null} */ (node.firstChild);
set_current_hydration_fragment(first_child === null ? [] : [first_child]);
} }
throw new Error('Expected a closing hydration marker');
} }

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

Loading…
Cancel
Save