Merge branch 'main' into main

pull/10936/head
Daniel 2 years ago committed by GitHub
commit 425a9a0347
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -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: use keys for hmr modules

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

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: further improvements to hmr component key generation

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

@ -0,0 +1,5 @@
---
"svelte": patch
---
feat: introduce `$host` rune, deprecate `createEventDispatcher`

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix missing classes after dynamic expressions in class attribute

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

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: execute sole static script tag

@ -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
---
fix: loosen proxy signal creation heuristics

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: make static `element` property available for the SvelteComponent type

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

@ -0,0 +1,5 @@
---
'svelte': patch
---
feat: simplify HMR implementation

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

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: improve internal proxied state signal heuristic

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: add validation around disallowed sequence expressions to element attributes

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

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: keep sibling selectors when dealing with slots/render tags/`svelte:element` tags

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: ensure top level snippets are defined when binding to component prop

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

@ -0,0 +1,5 @@
---
"svelte": patch
---
breaking: robustify interop of exports and props in runes mode

@ -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
---
fix: use correct meta property for hmr key

@ -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
---
breaking: move compiler.cjs to compiler/index.js

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: ensure deep mutation ownership widening

@ -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

@ -0,0 +1,5 @@
---
"svelte": patch
---
feat: hot module reloading support for Svelte 5

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: improve compiled output of multiple call expression in single text node

@ -10,6 +10,7 @@
},
"changesets": [
"afraid-dogs-matter",
"afraid-geckos-dance",
"afraid-moose-matter",
"angry-books-jam",
"angry-plums-punch",
@ -21,15 +22,20 @@
"big-eggs-flash",
"big-eyes-carry",
"big-geese-act",
"big-moons-occur",
"blue-ants-raise",
"blue-rules-juggle",
"blue-timers-film",
"brave-gorillas-end",
"brave-points-sleep",
"brave-shrimps-kiss",
"brave-walls-destroy",
"brave-walls-flow",
"breezy-carrots-flash",
"breezy-rules-beg",
"bright-peas-juggle",
"bright-snakes-sing",
"brown-houses-obey",
"brown-months-fry",
"brown-spoons-boil",
"calm-ravens-sneeze",
@ -39,6 +45,7 @@
"chatty-taxis-juggle",
"chilled-pumas-invite",
"chilly-dolphins-lick",
"chilly-rocks-hug",
"chilly-snakes-scream",
"clean-eels-beg",
"clever-chefs-relate",
@ -46,12 +53,16 @@
"cold-birds-own",
"cold-masks-learn",
"cool-ants-leave",
"cool-comics-marry",
"cool-peas-lick",
"cool-rabbits-tickle",
"cool-roses-trade",
"cuddly-pianos-drop",
"cuddly-points-tickle",
"curly-lizards-dream",
"curvy-buses-laugh",
"curvy-cups-cough",
"curvy-flies-exercise",
"curvy-ties-shout",
"cyan-flowers-destroy",
"cyan-spies-grin",
@ -62,19 +73,24 @@
"dry-clocks-grow",
"dry-eggs-play",
"dry-eggs-retire",
"dry-pillows-exist",
"dull-coins-vanish",
"dull-mangos-wave",
"dull-pots-add",
"dull-roses-relate",
"early-ads-tie",
"early-months-whisper",
"eight-hornets-punch",
"eight-steaks-shout",
"eighty-bikes-camp",
"eighty-days-cheat",
"eleven-beers-yell",
"eleven-cycles-applaud",
"empty-bags-heal",
"empty-bulldogs-exercise",
"empty-crabs-think",
"empty-tools-share",
"fair-bags-smoke",
"fair-crabs-check",
"fair-pianos-talk",
"fair-spies-repeat",
@ -82,6 +98,7 @@
"famous-knives-sneeze",
"famous-pants-pay",
"fast-weeks-clean",
"few-clouds-shop",
"few-mugs-fail",
"fifty-rice-wait",
"fifty-steaks-float",
@ -95,18 +112,23 @@
"forty-peaches-unite",
"forty-suns-smile",
"four-flies-hammer",
"four-mice-hammer",
"fresh-dots-destroy",
"fresh-impalas-bow",
"fresh-weeks-trade",
"friendly-candles-relate",
"friendly-lies-camp",
"funny-wombats-argue",
"fuzzy-bags-camp",
"fuzzy-donuts-provide",
"gentle-dolls-juggle",
"gentle-sheep-hug",
"gentle-spies-happen",
"giant-moons-own",
"giant-planets-shake",
"giant-plants-grin",
"giant-roses-press",
"gold-tools-nail",
"good-buses-reply",
"good-cars-visit",
"good-pianos-jump",
@ -117,21 +139,25 @@
"green-eggs-approve",
"green-hounds-play",
"green-tigers-judge",
"grumpy-jars-sparkle",
"happy-beds-scream",
"happy-suits-film",
"healthy-planes-vanish",
"heavy-comics-move",
"heavy-ducks-leave",
"heavy-ears-rule",
"hip-balloons-begin",
"honest-buses-add",
"honest-dragons-turn",
"honest-icons-change",
"hot-jobs-tap",
"hungry-boxes-relate",
"hungry-dots-fry",
"hungry-singers-share",
"hungry-tips-unite",
"hungry-trees-travel",
"itchy-beans-melt",
"itchy-bulldogs-tan",
"itchy-kings-deliver",
"itchy-lions-wash",
"itchy-terms-guess",
@ -148,30 +174,37 @@
"large-clouds-carry",
"large-turkeys-deny",
"late-crabs-lay",
"late-peaches-mate",
"lazy-masks-sit",
"lazy-months-knock",
"lazy-spiders-think",
"lemon-geese-call",
"lemon-geese-drum",
"light-badgers-glow",
"light-days-clean",
"light-humans-hang",
"light-pens-watch",
"little-pans-jog",
"long-buckets-lay",
"long-crews-return",
"long-humans-repair",
"long-lobsters-mate",
"loud-cheetahs-flow",
"loud-mugs-smile",
"loud-ravens-drop",
"lovely-carpets-lick",
"lovely-houses-own",
"lovely-items-turn",
"lovely-rules-eat",
"lucky-schools-hang",
"lucky-toes-begin",
"many-rockets-give",
"many-trees-fix",
"metal-clouds-raise",
"metal-lobsters-burn",
"mighty-cooks-scream",
"mighty-files-hammer",
"mighty-frogs-obey",
"moody-carrots-lay",
"moody-frogs-exist",
"moody-houses-argue",
@ -181,8 +214,10 @@
"nasty-yaks-peel",
"neat-boats-shake",
"neat-dingos-clap",
"neat-files-rescue",
"nervous-spoons-relax",
"new-boats-wait",
"new-brooms-grin",
"new-rabbits-flow",
"nice-avocados-move",
"ninety-dingos-walk",
@ -198,6 +233,7 @@
"old-oranges-compete",
"olive-kangaroos-brake",
"olive-mice-fix",
"olive-moons-act",
"olive-seals-sell",
"olive-shirts-complain",
"olive-socks-kick",
@ -213,7 +249,9 @@
"poor-seahorses-flash",
"popular-ligers-perform",
"popular-mangos-rest",
"popular-walls-hunt",
"pretty-ties-help",
"proud-queens-sniff",
"purple-dragons-peel",
"quiet-apricots-dream",
"quiet-berries-end",
@ -225,10 +263,12 @@
"real-guests-do",
"real-items-suffer",
"real-pandas-brush",
"red-cycles-pretend",
"red-doors-own",
"red-feet-worry",
"red-poets-study",
"rich-cobras-exist",
"rich-garlics-laugh",
"rich-olives-yell",
"rich-sheep-burn",
"rich-tables-sing",
@ -245,6 +285,7 @@
"serious-gorillas-eat",
"serious-kids-deliver",
"serious-needles-joke",
"serious-poems-brake",
"serious-socks-cover",
"serious-zebras-scream",
"seven-deers-jam",
@ -262,6 +303,7 @@
"short-countries-rush",
"silent-apes-report",
"silly-laws-happen",
"silly-lies-film",
"silly-ways-wash",
"silver-points-approve",
"sixty-items-crash",
@ -276,6 +318,8 @@
"small-papayas-laugh",
"small-sheep-type",
"small-spiders-fail",
"smart-cherries-leave",
"smart-grapes-mate",
"smart-parents-swim",
"smart-turkeys-tell",
"smart-zebras-pay",
@ -297,6 +341,7 @@
"spotty-turkeys-sparkle",
"stale-books-perform",
"stale-comics-look",
"stale-fans-rest",
"stale-jeans-refuse",
"strange-apricots-happen",
"strong-gifts-smoke",
@ -307,6 +352,7 @@
"swift-donkeys-perform",
"swift-fans-stare",
"swift-feet-juggle",
"swift-poets-carry",
"swift-ravens-hunt",
"swift-seahorses-deliver",
"tall-books-grin",
@ -336,8 +382,11 @@
"thirty-pears-hug",
"thirty-wombats-relax",
"three-camels-sell",
"three-foxes-relax",
"three-icons-trade",
"three-lions-visit",
"three-papayas-buy",
"three-rice-tie",
"three-suits-grin",
"tidy-buses-whisper",
"tidy-starfishes-allow",
@ -359,6 +408,7 @@
"wicked-doors-train",
"wicked-hairs-cheer",
"wild-foxes-wonder",
"wild-moose-compare",
"wise-apples-care",
"wise-dancers-hang",
"wise-dodos-tell",
@ -370,6 +420,7 @@
"witty-steaks-dream",
"witty-tomatoes-care",
"witty-years-crash",
"yellow-taxis-double"
"yellow-taxis-double",
"young-masks-refuse"
]
}

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: improve hydration of svelte head blocks

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

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: improve handled of unowned derived signals

@ -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
---
fix: hydrate HTML with surrounding whitespace

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: generate correct types for reactive Map/Set/Date

@ -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: 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: bundle CSS types

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

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: addresses reactive Set bug in certain engines

@ -11,5 +11,5 @@ trim_trailing_whitespace = true
[test/**/expected.css]
insert_final_newline = false
[{package.json,.travis.yml,.eslintrc.json}]
[package.json]
indent_style = space

@ -1,23 +0,0 @@
# NOTE: In general this should be kept in sync with .eslintignore
**/dist/**
**/config/**
**/build/**
**/playgrounds/sandbox/**
**/npm/**
**/*.js.flow
**/*.d.ts
**/playwright*/**
**/vite.config.js
**/vite.prod.config.js
**/node_modules
**/tests/**
# documentation can contain invalid examples
documentation/**
# contains a fork of the REPL which doesn't adhere to eslint rules
sites/svelte-5-preview/**
# Wasn't checked previously, reenable at some point
sites/svelte.dev/**

@ -1,54 +0,0 @@
module.exports = {
extends: ['@sveltejs'],
// TODO: add runes to eslint-plugin-svelte
globals: {
$state: true,
$derived: true,
$effect: true,
$props: true
},
overrides: [
{
// scripts and playground should be console logging so don't lint against them
files: ['playgrounds/**/*', 'scripts/**/*'],
rules: {
'no-console': 'off'
}
},
{
// the playgrounds can use public naming conventions since they're examples
files: ['playgrounds/**/*'],
rules: {
'lube/svelte-naming-convention': 'off'
}
},
{
files: ['packages/svelte/src/compiler/**/*'],
rules: {
'no-var': 'error'
}
}
],
plugins: ['lube'],
rules: {
'no-console': 'error',
'lube/svelte-naming-convention': ['error', { fixSameNames: true }],
// eslint isn't that well-versed with JSDoc to know that `foo: /** @type{..} */ (foo)` isn't a violation of this rule, so turn it off
'object-shorthand': 'off',
'no-var': 'off',
// TODO: enable these rules and run `pnpm lint:fix`
// skipping that for now so as to avoid impacting real work
'@typescript-eslint/array-type': 'off',
'@typescript-eslint/no-namespace': 'off',
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/no-unused-vars': 'off',
'prefer-const': 'off',
'svelte/valid-compile': 'off',
quotes: 'off'
}
};

@ -1,5 +1,3 @@
# NOTE: In general this should be kept in sync with .eslintignore
packages/**/dist/*.js
packages/**/build/*.js
packages/**/npm/**/*
@ -10,12 +8,10 @@ packages/svelte/tests/**/_actual*
packages/svelte/tests/**/expected*
packages/svelte/tests/**/_output
packages/svelte/tests/**/shards/*.test.js
packages/svelte/tests/hydration/samples/*/_before.html
packages/svelte/tests/hydration/samples/*/_before_head.html
packages/svelte/tests/hydration/samples/*/_after.html
packages/svelte/tests/hydration/samples/*/_after_head.html
packages/svelte/tests/hydration/samples/*/_expected.html
packages/svelte/tests/hydration/samples/*/_override.html
packages/svelte/types
packages/svelte/compiler.cjs
packages/svelte/compiler/index.js
playgrounds/demo/src
playgrounds/sandbox/input/**.svelte
playgrounds/sandbox/output
@ -33,7 +29,6 @@ sites/svelte.dev/src/lib/generated
**/.vercel
.github/CODEOWNERS
.prettierignore
.eslintignore
.changeset
pnpm-lock.yaml
pnpm-workspace.yaml

@ -0,0 +1,45 @@
import svelte_config from '@sveltejs/eslint-config';
import lube from 'eslint-plugin-lube';
/** @type {import('eslint').Linter.FlatConfig[]} */
export default [
...svelte_config,
{
plugins: {
lube
},
rules: {
'no-console': 'error',
'lube/svelte-naming-convention': ['error', { fixSameNames: true }],
// eslint isn't that well-versed with JSDoc to know that `foo: /** @type{..} */ (foo)` isn't a violation of this rule, so turn it off
'object-shorthand': 'off',
'no-var': 'off',
// TODO: enable these rules and run `pnpm lint:fix`
// skipping that for now so as to avoid impacting real work
'@stylistic/quotes': 'off',
'@typescript-eslint/no-unused-vars': 'off',
'prefer-const': 'off'
}
},
{
files: ['playgrounds/**/*'],
rules: {
'lube/svelte-naming-convention': 'off',
'no-console': 'off'
}
},
{
ignores: [
'**/*.d.ts',
'**/tests',
'packages/svelte/compiler/index.js',
// documentation can contain invalid examples
'documentation',
// contains a fork of the REPL which doesn't adhere to eslint rules
'sites/svelte-5-preview/**',
// wasn't checked previously, reenable at some point
'sites/svelte.dev/**'
]
}
];

@ -19,28 +19,28 @@
"preview-site": "npm run build --prefix sites/svelte-5-preview",
"check": "cd packages/svelte && pnpm build && cd ../../ && pnpm -r check",
"format": "prettier --write .",
"lint": "prettier --check . && eslint ./",
"test": "vitest run --coverage",
"test-output": "vitest run --reporter=json --outputFile=sites/svelte-5-preview/src/routes/status/results.json",
"lint": "prettier --check . && eslint",
"test": "vitest run",
"test-output": "vitest run --coverage --reporter=json --outputFile=sites/svelte-5-preview/src/routes/status/results.json",
"changeset:version": "changeset version && pnpm -r generate:version && git add --all",
"changeset:publish": "changeset publish"
},
"devDependencies": {
"@changesets/cli": "^2.27.1",
"@sveltejs/eslint-config": "^6.0.4",
"@sveltejs/eslint-config": "^7.0.1",
"@svitejs/changesets-changelog-github-compact": "^1.1.0",
"@types/node": "^20.11.5",
"@typescript-eslint/eslint-plugin": "^6.21.0",
"@vitest/coverage-v8": "^1.2.1",
"concurrently": "^8.2.2",
"cross-env": "^7.0.3",
"eslint": "^8.56.0",
"eslint": "^9.0.0",
"eslint-plugin-lube": "^0.4.3",
"jsdom": "22.0.0",
"playwright": "^1.35.1",
"playwright": "^1.41.1",
"prettier": "^3.2.4",
"prettier-plugin-svelte": "^3.1.2",
"typescript": "^5.3.3",
"typescript-eslint": "^7.6.0",
"vitest": "^1.2.1"
},
"pnpm": {

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

@ -1,5 +1,183 @@
# svelte
## 5.0.0-next.102
### Patch Changes
- fix: generate correct types for reactive Map/Set/Date ([#11153](https://github.com/sveltejs/svelte/pull/11153))
## 5.0.0-next.101
### Patch Changes
- fix missing classes after dynamic expressions in class attribute ([#11134](https://github.com/sveltejs/svelte/pull/11134))
- feat: simplify HMR implementation ([#11132](https://github.com/sveltejs/svelte/pull/11132))
- fix: add validation around disallowed sequence expressions to element attributes ([#11149](https://github.com/sveltejs/svelte/pull/11149))
## 5.0.0-next.100
### Patch Changes
- fix: further improvements to hmr component key generation ([#11129](https://github.com/sveltejs/svelte/pull/11129))
## 5.0.0-next.99
### Patch Changes
- fix: use correct meta property for hmr key ([#11125](https://github.com/sveltejs/svelte/pull/11125))
## 5.0.0-next.98
### Patch Changes
- fix: use keys for hmr modules ([#11123](https://github.com/sveltejs/svelte/pull/11123))
- fix: addresses reactive Set bug in certain engines ([#11120](https://github.com/sveltejs/svelte/pull/11120))
## 5.0.0-next.97
### Patch Changes
- fix: loosen proxy signal creation heuristics ([#11109](https://github.com/sveltejs/svelte/pull/11109))
- fix: ensure top level snippets are defined when binding to component prop ([#11104](https://github.com/sveltejs/svelte/pull/11104))
- feat: hot module reloading support for Svelte 5 ([#11106](https://github.com/sveltejs/svelte/pull/11106))
## 5.0.0-next.96
### Patch Changes
- feat: introduce `$host` rune, deprecate `createEventDispatcher` ([#11059](https://github.com/sveltejs/svelte/pull/11059))
- fix: execute sole static script tag ([#11095](https://github.com/sveltejs/svelte/pull/11095))
- fix: make static `element` property available for the SvelteComponent type ([#11079](https://github.com/sveltejs/svelte/pull/11079))
- fix: improve internal proxied state signal heuristic ([#11102](https://github.com/sveltejs/svelte/pull/11102))
- fix: keep sibling selectors when dealing with slots/render tags/`svelte:element` tags ([#11096](https://github.com/sveltejs/svelte/pull/11096))
- fix: ensure deep mutation ownership widening ([#11094](https://github.com/sveltejs/svelte/pull/11094))
- fix: improve compiled output of multiple call expression in single text node ([#11097](https://github.com/sveltejs/svelte/pull/11097))
- fix: improve hydration of svelte head blocks ([#11099](https://github.com/sveltejs/svelte/pull/11099))
## 5.0.0-next.95
### Patch Changes
- breaking: robustify interop of exports and props in runes mode ([#11064](https://github.com/sveltejs/svelte/pull/11064))
- fix: improve handled of unowned derived signals ([#11077](https://github.com/sveltejs/svelte/pull/11077))
- fix: bundle CSS types ([#11067](https://github.com/sveltejs/svelte/pull/11067))
## 5.0.0-next.94
### Patch Changes
- fix: add `anchor` support to mount() API ([#11050](https://github.com/sveltejs/svelte/pull/11050))
## 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

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

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

@ -2,7 +2,7 @@
"name": "svelte",
"description": "Cybernetically enhanced web apps",
"license": "MIT",
"version": "5.0.0-next.83",
"version": "5.0.0-next.102",
"type": "module",
"types": "./types/index.d.ts",
"engines": {
@ -12,17 +12,17 @@
"src",
"!src/**/*.test.*",
"types",
"compiler.cjs",
"compiler",
"*.d.ts",
"README.md"
],
"module": "src/main/main-client.js",
"main": "src/main/main-client.js",
"module": "src/index-client.js",
"main": "src/index-client.js",
"exports": {
".": {
"types": "./types/index.d.ts",
"browser": "./src/main/main-client.js",
"default": "./src/main/main-server.js"
"browser": "./src/index-client.js",
"default": "./src/index-server.js"
},
"./package.json": "./package.json",
"./action": {
@ -34,7 +34,7 @@
},
"./compiler": {
"types": "./types/index.d.ts",
"require": "./compiler.cjs",
"require": "./compiler/index.js",
"default": "./src/compiler/index.js"
},
"./easing": {
@ -47,6 +47,9 @@
"./internal": {
"default": "./src/internal/index.js"
},
"./internal/client": {
"default": "./src/internal/client/index.js"
},
"./internal/disclose-version": {
"default": "./src/internal/disclose-version.js"
},
@ -64,7 +67,8 @@
},
"./reactivity": {
"types": "./types/index.d.ts",
"default": "./src/reactivity/index.js"
"browser": "./src/reactivity/index-client.js",
"default": "./src/reactivity/index-server.js"
},
"./server": {
"types": "./types/index.d.ts",
@ -100,7 +104,7 @@
"check": "tsc && cd ./tests/types && tsc",
"check:watch": "tsc --watch",
"generate:version": "node ./scripts/generate-version.js",
"generate:types": "node ./scripts/generate-types.js",
"generate:types": "node ./scripts/generate-types.js && tsc -p tsconfig.generated.json",
"prepublishOnly": "pnpm build",
"knip": "knip"
},
@ -112,7 +116,7 @@
"@rollup/plugin-terser": "^0.4.4",
"@rollup/plugin-virtual": "^3.0.2",
"@types/aria-query": "^5.0.4",
"dts-buddy": "^0.4.4",
"dts-buddy": "^0.4.7",
"esbuild": "^0.19.11",
"knip": "^4.2.1",
"rollup": "^4.9.5",
@ -137,7 +141,9 @@
"knip": {
"entry": [
"src/*/index.js",
"src/*/public.d.ts"
"src/index-client.ts",
"src/index-server.ts",
"src/index.d.ts"
],
"project": [
"src/**"

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

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

@ -22,14 +22,14 @@ fs.writeFileSync(`${dir}/types/compiler/interfaces.d.ts`, "import '../index.js';
await createBundle({
output: `${dir}/types/index.d.ts`,
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}/animate`]: `${dir}/src/animate/public.d.ts`,
[`${pkg.name}/compiler`]: `${dir}/src/compiler/index.js`,
[`${pkg.name}/easing`]: `${dir}/src/easing/index.js`,
[`${pkg.name}/legacy`]: `${dir}/src/legacy/legacy-client.js`,
[`${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}/store`]: `${dir}/src/store/public.d.ts`,
[`${pkg.name}/transition`]: `${dir}/src/transition/public.d.ts`,

@ -211,3 +211,24 @@ declare function $bindable<T>(t?: T): T;
declare function $inspect<T extends any[]>(
...values: T
): { with: (fn: (type: 'init' | 'update', ...values: T) => void) => void };
/**
* Retrieves the `this` reference of the custom element that contains this component. Example:
*
* ```svelte
* <svelte:options customElement="my-element" />
*
* <script>
* function greet(greeting) {
* $host().dispatchEvent(new CustomEvent('greeting', { detail: greeting }))
* }
* </script>
*
* <button onclick={() => greet('hello')}>say hello</button>
* ```
*
* Only available inside custom element components, and only on the client-side.
*
* https://svelte-5-preview.vercel.app/docs/runes#$host
*/
declare function $host<El extends HTMLElement = HTMLElement>(): El;

@ -187,6 +187,8 @@ const runes = {
'invalid-state-location': (rune) =>
`${rune}(...) can only be used as a variable declaration initializer or a class field`,
'invalid-effect-location': () => `$effect() can only be used as an expression statement`,
'invalid-host-location': () =>
`$host() can only be used inside custom element component instances`,
/**
* @param {boolean} is_binding
* @param {boolean} show_details
@ -212,7 +214,9 @@ const runes = {
'duplicate-props-rune': () => `Cannot use $props() more than once`,
'invalid-each-assignment': () =>
`Cannot reassign or bind to each block argument in runes mode. Use the array and index variables instead (e.g. 'array[i] = value' instead of 'entry = value')`,
'invalid-derived-call': () => `$derived.call(...) has been replaced with $derived.by(...)`
'invalid-derived-call': () => `$derived.call(...) has been replaced with $derived.by(...)`,
'conflicting-property-name': () =>
`Cannot have a property and a component export with the same name`
};
/** @satisfies {Errors} */
@ -282,7 +286,9 @@ const attributes = {
},
'invalid-let-directive-placement': () => 'let directive at invalid position',
'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} */

@ -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);
return result;

@ -36,7 +36,8 @@ export default function read_style(parser, start, attributes) {
content: {
start: content_start,
end: content_end,
styles: parser.template.slice(content_start, content_end)
styles: parser.template.slice(content_start, content_end),
comment: null
}
};
}

@ -283,25 +283,26 @@ export default function tag(parser) {
if (is_top_level_script_or_style) {
parser.eat('>', true);
if (name === 'script') {
const content = read_script(parser, start, element.attributes);
/** @type {import('#compiler').Comment | null} */
let prev_comment = null;
for (let i = current.fragment.nodes.length - 1; i >= 0; i--) {
const node = current.fragment.nodes[i];
/** @type {import('#compiler').Comment | null} */
let prev_comment = null;
for (let i = current.fragment.nodes.length - 1; i >= 0; i--) {
const node = current.fragment.nodes[i];
if (i === current.fragment.nodes.length - 1 && node.end !== start) {
break;
}
if (i === current.fragment.nodes.length - 1 && node.end !== start) {
break;
}
if (node.type === 'Comment') {
prev_comment = node;
break;
} else if (node.type !== 'Text' || node.data.trim()) {
break;
}
if (node.type === 'Comment') {
prev_comment = node;
break;
} else if (node.type !== 'Text' || node.data.trim()) {
break;
}
}
if (name === 'script') {
const content = read_script(parser, start, element.attributes);
if (prev_comment) {
// We take advantage of the fact that the root will never have leadingComments set,
// and set the previous comment to it so that the warning mechanism can later
@ -318,6 +319,7 @@ export default function tag(parser) {
}
} else {
const content = read_style(parser, start, element.attributes);
content.content.comment = prev_comment;
if (current.css) error(start, 'duplicate-style-element');
current.css = content;

@ -175,7 +175,13 @@ function apply_selector(relative_selectors, rule, element, stylesheet) {
let sibling_matched = false;
for (const possible_sibling of siblings.keys()) {
if (apply_selector(parent_selectors, rule, possible_sibling, stylesheet)) {
if (possible_sibling.type === 'RenderTag' || possible_sibling.type === 'SlotElement') {
// `{@render foo()}<p>foo</p>` with `:global(.x) + p` is a match
if (parent_selectors.length === 1 && parent_selectors[0].metadata.is_global) {
mark(relative_selector, element);
sibling_matched = true;
}
} else if (apply_selector(parent_selectors, rule, possible_sibling, stylesheet)) {
mark(relative_selector, element);
sibling_matched = true;
}
@ -564,38 +570,39 @@ function get_element_parent(node) {
function find_previous_sibling(node) {
/** @type {import('#compiler').SvelteNode} */
let current_node = node;
do {
if (current_node.type === 'SlotElement') {
const slot_children = current_node.fragment.nodes;
if (slot_children.length > 0) {
current_node = slot_children.slice(-1)[0]; // go to its last child first
continue;
}
}
while (
// @ts-expect-error TODO
!current_node.prev &&
// @ts-expect-error TODO
current_node.parent &&
// @ts-expect-error TODO
current_node.parent.type === 'SlotElement'
) {
// @ts-expect-error TODO
current_node = current_node.parent;
while (
// @ts-expect-error TODO
!current_node.prev &&
// @ts-expect-error TODO
current_node.parent?.type === 'SlotElement'
) {
// @ts-expect-error TODO
current_node = current_node.parent;
}
// @ts-expect-error
current_node = current_node.prev;
while (current_node?.type === 'SlotElement') {
const slot_children = current_node.fragment.nodes;
if (slot_children.length > 0) {
current_node = slot_children.slice(-1)[0];
} else {
break;
}
// @ts-expect-error
current_node = current_node.prev;
} while (current_node && current_node.type === 'SlotElement');
}
return current_node;
}
/**
* @param {import('#compiler').SvelteNode} node
* @param {boolean} adjacent_only
* @returns {Map<import('#compiler').RegularElement, NodeExistsValue>}
* @returns {Map<import('#compiler').RegularElement | import('#compiler').SvelteElement | import('#compiler').SlotElement | import('#compiler').RenderTag, NodeExistsValue>}
*/
function get_possible_element_siblings(node, adjacent_only) {
/** @type {Map<import('#compiler').RegularElement, NodeExistsValue>} */
/** @type {Map<import('#compiler').RegularElement | import('#compiler').SvelteElement | import('#compiler').SlotElement | import('#compiler').RenderTag, NodeExistsValue>} */
const result = new Map();
/** @type {import('#compiler').SvelteNode} */
@ -618,6 +625,14 @@ function get_possible_element_siblings(node, adjacent_only) {
if (adjacent_only && has_definite_elements(possible_last_child)) {
return result;
}
} else if (
prev.type === 'SlotElement' ||
prev.type === 'RenderTag' ||
prev.type === 'SvelteElement'
) {
result.set(prev, NODE_PROBABLY_EXISTS);
// Special case: slots, render tags and svelte:element tags could resolve to no siblings,
// so we want to continue until we find a definite sibling even with the adjacent-only combinator
}
}
@ -720,7 +735,7 @@ function get_possible_last_child(relative_selector, adjacent_only) {
}
/**
* @param {Map<import('#compiler').RegularElement, NodeExistsValue>} result
* @param {Map<unknown, NodeExistsValue>} result
* @returns {boolean}
*/
function has_definite_elements(result) {
@ -734,8 +749,9 @@ function has_definite_elements(result) {
}
/**
* @param {Map<import('#compiler').RegularElement, NodeExistsValue>} from
* @param {Map<import('#compiler').RegularElement, NodeExistsValue>} to
* @template T
* @param {Map<T, NodeExistsValue>} from
* @param {Map<T, NodeExistsValue>} to
* @returns {void}
*/
function add_to_map(from, to) {

@ -0,0 +1,34 @@
import { walk } from 'zimmerframe';
import { warn } from '../../../warnings.js';
import { is_keyframes_node } from '../../css.js';
/**
* @param {import('#compiler').Css.StyleSheet} stylesheet
* @param {import('../../types.js').RawWarning[]} warnings
*/
export function warn_unused(stylesheet, warnings) {
walk(stylesheet, { warnings, stylesheet }, visitors);
}
/** @type {import('zimmerframe').Visitors<import('#compiler').Css.Node, { warnings: import('../../types.js').RawWarning[], stylesheet: import('#compiler').Css.StyleSheet }>} */
const visitors = {
Atrule(node, context) {
if (!is_keyframes_node(node)) {
context.next();
}
},
PseudoClassSelector(node, context) {
if (node.name === 'is' || node.name === 'where') {
context.next();
}
},
ComplexSelector(node, context) {
if (!node.metadata.used) {
const content = context.state.stylesheet.content;
const text = content.styles.substring(node.start - content.start, node.end - content.start);
warn(context.state.warnings, node, context.path, 'css-unused-selector', text);
}
context.next();
}
};

@ -23,6 +23,7 @@ import { should_proxy_or_freeze } from '../3-transform/client/utils.js';
import { analyze_css } from './css/css-analyze.js';
import { prune } from './css/css-prune.js';
import { hash } from './utils.js';
import { warn_unused } from './css/css-warn.js';
/**
* @param {import('#compiler').Script | null} script
@ -258,10 +259,11 @@ export function analyze_module(ast, options) {
/**
* @param {import('#compiler').Root} root
* @param {string} source
* @param {import('#compiler').ValidatedCompileOptions} options
* @returns {import('../types.js').ComponentAnalysis}
*/
export function analyze_component(root, options) {
export function analyze_component(root, source, options) {
const scope_root = new ScopeRoot();
const module = js(root.module, scope_root, false, null);
@ -396,7 +398,8 @@ export function analyze_component(root, options) {
})
: '',
keyframes: []
}
},
source
};
if (!options.customElement && root.options?.customElement) {
@ -435,6 +438,20 @@ export function analyze_component(root, options) {
merge(set_scope(scopes), validation_runes, runes_scope_tweaker, common_visitors)
);
}
if (analysis.exports.length > 0) {
for (const [_, binding] of instance.scope.declarations) {
if (binding.kind === 'prop' || binding.kind === 'bindable_prop') {
if (
analysis.exports.some(
({ alias, name }) => (binding.prop_alias ?? binding.node.name) === (alias ?? name)
)
) {
error(binding.node, 'conflicting-property-name');
}
}
}
}
} else {
instance.scope.declare(b.id('$$props'), 'bindable_prop', 'synthetic');
instance.scope.declare(b.id('$$restProps'), 'rest_prop', 'synthetic');
@ -532,6 +549,7 @@ export function analyze_component(root, options) {
for (const element of analysis.elements) {
prune(analysis.css.ast, element);
}
warn_unused(analysis.css.ast, analysis.warnings);
outer: for (const element of analysis.elements) {
if (element.metadata.scoped) {
@ -608,20 +626,38 @@ const legacy_scope_tweaker = {
/** @type {import('../types.js').ReactiveStatement} */
const reactive_statement = {
assignments: new Set(),
dependencies: new Set()
dependencies: []
};
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) {
const binding = state.scope.get(name);
if (binding === null) continue;
// Include bindings that have references other than assignments and their own declarations
if (
nodes.some((n) => n.node !== binding.node && !reactive_statement.assignments.has(n.node))
) {
reactive_statement.dependencies.add(binding);
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 (
parent.type === 'AssignmentExpression' &&
parent.operator === '=' &&
parent.left === left
) {
continue;
}
reactive_statement.dependencies.push(binding);
break;
}
}
@ -630,8 +666,8 @@ const legacy_scope_tweaker = {
// Ideally this would be in the validation file, but that isn't possible because this visitor
// calls "next" before setting the reactive statements.
if (
reactive_statement.dependencies.size &&
[...reactive_statement.dependencies].every(
reactive_statement.dependencies.length &&
reactive_statement.dependencies.every(
(d) => d.scope === state.analysis.module.scope && d.declaration_kind !== 'const'
)
) {
@ -660,15 +696,29 @@ const legacy_scope_tweaker = {
}
},
AssignmentExpression(node, { state, next }) {
if (state.reactive_statement && node.operator === '=') {
if (node.left.type === 'MemberExpression') {
const id = object(node.left);
if (id !== null) {
state.reactive_statement.assignments.add(id);
}
} else {
if (state.reactive_statement) {
const id = node.left.type === 'MemberExpression' ? object(node.left) : node.left;
if (id !== null) {
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);
}
}
}
@ -1343,21 +1393,21 @@ function order_reactive_statements(unsorted_reactive_declarations) {
const lookup = new Map();
for (const [node, declaration] of unsorted_reactive_declarations) {
declaration.assignments.forEach(({ name }) => {
const statements = lookup.get(name) ?? [];
for (const binding of declaration.assignments) {
const statements = lookup.get(binding.node.name) ?? [];
statements.push([node, declaration]);
lookup.set(name, statements);
});
lookup.set(binding.node.name, statements);
}
}
/** @type {Array<[string, string]>} */
const edges = [];
for (const [, { assignments, dependencies }] of unsorted_reactive_declarations) {
for (const { name } of assignments) {
for (const { node } of dependencies) {
if (![...assignments].find(({ name }) => node.name === name)) {
edges.push([name, node.name]);
for (const assignment of assignments) {
for (const dependency of dependencies) {
if (!assignments.has(dependency)) {
edges.push([assignment.node.name, dependency.node.name]);
}
}
}
@ -1381,14 +1431,17 @@ function order_reactive_statements(unsorted_reactive_declarations) {
*/
const add_declaration = (node, declaration) => {
if ([...reactive_declarations.values()].includes(declaration)) return;
declaration.dependencies.forEach(({ node: { name } }) => {
if ([...declaration.assignments].some((a) => a.name === name)) return;
for (const [node, earlier] of lookup.get(name) ?? []) {
for (const binding of declaration.dependencies) {
if (declaration.assignments.has(binding)) continue;
for (const [node, earlier] of lookup.get(binding.node.name) ?? []) {
add_declaration(node, earlier);
}
});
}
reactive_declarations.set(node, declaration);
};
for (const [node, declaration] of unsorted_reactive_declarations) {
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 (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);
if (attribute.name === 'slot') {
@ -81,12 +93,26 @@ function validate_element(node, context) {
for (const attribute of node.attributes) {
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') {
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');
}
}
}
if (regex_illegal_attribute_character.test(attribute.name)) {
error(attribute, 'invalid-attribute-name', attribute.name);
}
if (attribute.name.startsWith('on') && attribute.name.length > 2) {
if (!is_expression_attribute(attribute)) {
if (!is_expression) {
error(attribute, 'invalid-event-attribute-value');
}
@ -875,6 +901,9 @@ export const validation_runes_js = {
}
},
CallExpression(node, { state, path }) {
if (get_rune(node, state.scope) === '$host') {
error(node, 'invalid-host-location');
}
validate_call_expression(node, state.scope, path);
},
VariableDeclarator(node, { state }) {
@ -1042,9 +1071,17 @@ export const validation_runes = merge(validation, a11y_validators, {
}
},
CallExpression(node, { state, path }) {
if (get_rune(node, state.scope) === '$bindable' && node.arguments.length > 1) {
const rune = get_rune(node, state.scope);
if (rune === '$bindable' && node.arguments.length > 1) {
error(node, 'invalid-rune-args-length', '$bindable', [0, 1]);
} else if (rune === '$host') {
if (node.arguments.length > 0) {
error(node, 'invalid-rune-args-length', '$host', [0]);
} else if (state.ast_type === 'module' || !state.analysis.custom_element) {
error(node, 'invalid-host-location');
}
}
validate_call_expression(node, state.scope, path);
},
EachBlock(node, { next, state }) {

@ -46,9 +46,16 @@ export function client_component(source, analysis, options) {
options,
scope: analysis.module.scope,
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
// these should be set by create_block - if they're called outside, it's a bug
get before_init() {
/** @type {any[]} */
const a = [];
a.push = () =>
error(null, 'INTERNAL', 'before_init.push should not be called outside create_block');
return a;
},
get init() {
/** @type {any[]} */
const a = [];
@ -248,8 +255,7 @@ export function client_component(source, analysis, options) {
);
if (analysis.runes && options.dev) {
/** @type {import('estree').Literal[]} */
const bindable = [];
const bindable = analysis.exports.map(({ name, alias }) => b.literal(alias ?? name));
for (const [name, binding] of properties) {
if (binding.kind === 'bindable_prop') {
bindable.push(b.literal(binding.prop_alias ?? name));
@ -268,7 +274,7 @@ export function client_component(source, analysis, options) {
const setter = b.set(key, [
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) {
@ -375,7 +381,6 @@ export function client_component(source, analysis, options) {
);
if (analysis.uses_rest_props) {
/** @type {string[]} */
const named_props = analysis.exports.map(({ name, alias }) => alias ?? name);
for (const [name, binding] of analysis.instance.scope.declarations) {
if (binding.kind === 'bindable_prop') named_props.push(binding.prop_alias ?? name);
@ -394,15 +399,12 @@ export function client_component(source, analysis, options) {
}
if (analysis.uses_props || analysis.uses_rest_props) {
const to_remove = [b.literal('children'), b.literal('$$slots'), b.literal('$$events')];
if (analysis.custom_element) {
to_remove.push(b.literal('$$host'));
}
component_block.body.unshift(
b.const(
'$$sanitized_props',
b.call(
'$.rest_props',
b.id('$$props'),
b.array([b.literal('children'), b.literal('$$slots'), b.literal('$$events')])
)
)
b.const('$$sanitized_props', b.call('$.rest_props', b.id('$$props'), b.array(to_remove)))
);
}
@ -413,15 +415,38 @@ export function client_component(source, analysis, options) {
const body = [
...state.hoisted,
...module.body,
b.export_default(
b.function_declaration(
b.id(analysis.name),
[b.id('$$anchor'), b.id('$$props')],
component_block
)
b.function_declaration(
b.id(analysis.name),
[b.id('$$anchor'), b.id('$$props')],
component_block
)
];
if (options.hmr) {
body.push(
b.if(
b.id('import.meta.hot'),
b.block([
b.const(b.id('s'), b.call('$.source', b.id(analysis.name))),
b.stmt(b.assignment('=', b.id(analysis.name), b.call('$.hmr', b.id('s')))),
b.stmt(
b.call(
'import.meta.hot.accept',
b.arrow(
[b.id('module')],
b.block([
b.stmt(b.call('$.set', b.id('s'), b.member(b.id('module'), b.id('default'))))
])
)
)
)
])
)
);
}
body.push(b.export_default(b.id(analysis.name)));
if (options.dev) {
if (options.filename) {
let filename = options.filename;
@ -575,6 +600,6 @@ export function client_module(analysis, options) {
return {
type: 'Program',
sourceType: 'module',
body: [b.import_all('$', 'svelte/internal'), ...module.body]
body: [b.import_all('$', 'svelte/internal/client'), ...module.body]
};
}

@ -29,15 +29,12 @@ export interface ComponentClientTransformState extends ClientTransformState {
readonly hoisted: Array<Statement | ModuleDeclaration>;
readonly events: Set<string>;
/** Stuff that happens before the render effect(s) */
readonly before_init: Statement[];
/** Stuff that happens before the render effect(s) */
readonly init: Statement[];
/** Stuff that happens inside the render effect */
readonly update: {
/** 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;
}[];
readonly update: Statement[];
/** Stuff that happens after the render effect (control blocks, dynamic elements, bindings, actions, etc) */
readonly after_update: Statement[];
/** The HTML template string */

@ -195,8 +195,7 @@ export const javascript_visitors_runes = {
if (rune === '$props') {
assert.equal(declarator.id.type, 'ObjectPattern');
/** @type {string[]} */
const seen = [];
const seen = state.analysis.exports.map(({ name, alias }) => alias ?? name);
for (const property of declarator.id.properties) {
if (property.type === 'Property') {
@ -301,7 +300,7 @@ export const javascript_visitors_runes = {
declarations.push(
b.declarator(
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(
@ -371,7 +370,7 @@ export const javascript_visitors_runes = {
const func = context.visit(node.expression.arguments[0]);
return {
...node,
expression: b.call('$.pre_effect', /** @type {import('estree').Expression} */ (func))
expression: b.call('$.user_pre_effect', /** @type {import('estree').Expression} */ (func))
};
}
}
@ -381,6 +380,10 @@ export const javascript_visitors_runes = {
CallExpression(node, context) {
const rune = get_rune(node, context.state.scope);
if (rune === '$host') {
return b.id('$$props.$$host');
}
if (rune === '$effect.active') {
return b.call('$.effect_active');
}
@ -389,7 +392,7 @@ export const javascript_visitors_runes = {
const args = /** @type {import('estree').Expression[]} */ (
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') {

@ -24,11 +24,17 @@ import { create_attribute, is_custom_element_node, is_element_node } from '../..
import { error } from '../../../errors.js';
import { binding_properties } from '../../bindings.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 { BLOCK_CLOSE, BLOCK_CLOSE_ELSE } from '../../../../internal/server/hydration.js';
const block_open = t_string('<![>');
const block_close = t_string('<!]>');
export const block_open = t_string(`<!--${HYDRATION_START}-->`);
export const block_close = t_string(`<!--${HYDRATION_END}-->`);
/**
* @param {string} value
@ -685,7 +691,8 @@ const javascript_visitors_runes = {
}
if (rune === '$props') {
// remove $bindable() from props declaration
// remove $bindable() from props declaration and handle rest props
let uses_rest_props = false;
const id = walk(declarator.id, null, {
AssignmentPattern(node) {
if (
@ -697,9 +704,26 @@ const javascript_visitors_runes = {
: b.id('undefined');
return b.assignment_pattern(node.left, right);
}
},
RestElement(node, { path }) {
if (path.at(-1) === declarator.id) {
uses_rest_props = true;
}
}
});
declarations.push(b.declarator(id, b.id('$$props')));
const exports = /** @type {import('../../types').ComponentAnalysis} */ (
state.analysis
).exports.map(({ name, alias }) => b.literal(alias ?? name));
declarations.push(
b.declarator(
id,
uses_rest_props && exports.length > 0
? b.call('$.rest_props', b.id('$$props'), b.array(exports))
: b.id('$$props')
)
);
continue;
}
@ -761,6 +785,10 @@ const javascript_visitors_runes = {
CallExpression(node, context) {
const rune = get_rune(node, context.state.scope);
if (rune === '$host') {
return b.id('undefined');
}
if (rune === '$effect.active') {
return b.literal(false);
}
@ -825,9 +853,7 @@ function serialize_attribute_value(
/** @type {import('estree').Expression[]} */
const expressions = [];
if (attribute_value[0].type !== 'Text') {
quasis.push(b.quasi('', false));
}
quasis.push(b.quasi('', false));
let i = 0;
for (const node of attribute_value) {
@ -838,7 +864,8 @@ function serialize_attribute_value(
// don't trim, space could be important to separate from expression tag
data = data.replace(regex_whitespaces_strict, ' ');
}
quasis.push(b.quasi(data, i === attribute_value.length));
const last = /** @type {import('estree').TemplateElement} */ (quasis.at(-1));
last.value.raw += data;
} else {
expressions.push(
b.call(
@ -846,9 +873,7 @@ function serialize_attribute_value(
/** @type {import('estree').Expression} */ (context.visit(node.expression))
)
);
if (i === attribute_value.length || attribute_value[i]?.type !== 'Text') {
quasis.push(b.quasi('', true));
}
quasis.push(b.quasi('', i + 1 === attribute_value.length));
}
}
@ -1499,55 +1524,46 @@ const template_visitors = {
b.update('++', index, false),
b.block(each)
);
const close = b.stmt(b.assignment('+=', b.id('$$payload.out'), b.literal(BLOCK_CLOSE)));
if (node.fallback) {
const fallback_stmts = create_block(node, node.fallback.nodes, context);
fallback_stmts.unshift(
b.stmt(b.assignment('+=', b.id('$$payload.out'), b.literal('<!ssr:each_else>')))
);
const fallback = create_block(node, node.fallback.nodes, context);
fallback.push(b.stmt(b.assignment('+=', b.id('$$payload.out'), b.literal(BLOCK_CLOSE_ELSE))));
state.template.push(
t_statement(
b.if(
b.binary('!==', b.member(array_id, b.id('length')), b.literal(0)),
for_loop,
b.block(fallback_stmts)
b.block([for_loop, close]),
b.block(fallback)
)
)
);
} else {
state.template.push(t_statement(for_loop));
state.template.push(t_statement(for_loop), t_statement(close));
}
state.template.push(block_close);
},
IfBlock(node, context) {
const state = context.state;
state.template.push(block_open);
// 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);
consequent.unshift(
b.stmt(b.assignment('+=', b.id('$$payload.out'), b.literal('<!ssr:if:true>')))
);
const alternate = node.alternate ? create_block(node, node.alternate.nodes, context) : [];
const alternate = node.alternate
? /** @type {import('estree').BlockStatement} */ (context.visit(node.alternate))
: b.block([]);
alternate.body.unshift(
b.stmt(b.assignment('+=', b.id('$$payload.out'), b.literal('<!ssr:if:false>')))
);
consequent.push(b.stmt(b.assignment('+=', b.id('$$payload.out'), b.literal(BLOCK_CLOSE))));
alternate.push(b.stmt(b.assignment('+=', b.id('$$payload.out'), b.literal(BLOCK_CLOSE_ELSE))));
state.template.push(
t_statement(
b.if(
/** @type {import('estree').Expression} */ (context.visit(node.test)),
b.block(/** @type {import('estree').Statement[]} */ (consequent)),
alternate
b.block(consequent),
b.block(alternate)
)
)
);
state.template.push(block_close);
},
AwaitBlock(node, context) {
const state = context.state;
@ -1595,14 +1611,15 @@ const template_visitors = {
state.template.push(block_close);
},
SnippetBlock(node, context) {
// TODO hoist where possible
context.state.init.push(
b.function_declaration(
node.expression,
[b.id('$$payload'), ...node.parameters],
/** @type {import('estree').BlockStatement} */ (context.visit(node.body))
)
const fn = b.function_declaration(
node.expression,
[b.id('$$payload'), ...node.parameters],
/** @type {import('estree').BlockStatement} */ (context.visit(node.body))
);
// @ts-expect-error - TODO remove this hack once $$render_inner for legacy bindings is gone
fn.___snippet = true;
// TODO hoist where possible
context.state.init.push(fn);
if (context.state.options.dev) {
context.state.init.push(b.stmt(b.call('$.add_snippet_symbol', node.expression)));
@ -2202,14 +2219,27 @@ 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 legacy bindings are stable.
// We can remove this once the legacy syntax is gone.
if (analysis.uses_component_bindings) {
const snippets = template.body.filter(
(node) =>
node.type === 'FunctionDeclaration' &&
// @ts-expect-error
node.___snippet
);
const rest = template.body.filter(
(node) =>
node.type !== 'FunctionDeclaration' ||
// @ts-expect-error
!node.___snippet
);
template.body = [
...snippets,
b.let('$$settled', b.true),
b.let('$$inner_payload'),
b.stmt(
b.function(
b.id('$$render_inner'),
[b.id('$$payload')],
b.block(/** @type {import('estree').Statement[]} */ (template.body))
b.block(/** @type {import('estree').Statement[]} */ (rest))
)
),
b.do_while(

@ -40,7 +40,8 @@ export const Runes = /** @type {const} */ ([
'$effect.active',
'$effect.root',
'$inspect',
'$inspect().with'
'$inspect().with',
'$host'
]);
/**

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

@ -0,0 +1,153 @@
import type { Comment } from '#compiler';
export namespace Css {
export interface BaseNode {
start: number;
end: number;
}
export interface StyleSheet extends BaseNode {
type: 'StyleSheet';
attributes: any[]; // TODO
children: Array<Atrule | Rule>;
content: {
start: number;
end: number;
styles: string;
/** Possible comment atop the style tag */
comment: Comment | null;
};
}
export interface Atrule extends BaseNode {
type: 'Atrule';
name: string;
prelude: string;
block: Block | null;
}
export interface Rule extends BaseNode {
type: 'Rule';
prelude: SelectorList;
block: Block;
metadata: {
parent_rule: null | Rule;
has_local_selectors: boolean;
};
}
export interface SelectorList extends BaseNode {
type: 'SelectorList';
children: ComplexSelector[];
}
export interface ComplexSelector extends BaseNode {
type: 'ComplexSelector';
children: RelativeSelector[];
metadata: {
rule: null | Rule;
used: boolean;
};
}
export interface RelativeSelector extends BaseNode {
type: 'RelativeSelector';
combinator: null | Combinator;
selectors: SimpleSelector[];
metadata: {
is_global: boolean;
is_host: boolean;
is_root: boolean;
scoped: boolean;
};
}
export interface TypeSelector extends BaseNode {
type: 'TypeSelector';
name: string;
}
export interface IdSelector extends BaseNode {
type: 'IdSelector';
name: string;
}
export interface ClassSelector extends BaseNode {
type: 'ClassSelector';
name: string;
}
export interface AttributeSelector extends BaseNode {
type: 'AttributeSelector';
name: string;
matcher: string | null;
value: string | null;
flags: string | null;
}
export interface PseudoElementSelector extends BaseNode {
type: 'PseudoElementSelector';
name: string;
}
export interface PseudoClassSelector extends BaseNode {
type: 'PseudoClassSelector';
name: string;
args: SelectorList | null;
}
export interface Percentage extends BaseNode {
type: 'Percentage';
value: string;
}
export interface NestingSelector extends BaseNode {
type: 'NestingSelector';
name: '&';
}
export interface Nth extends BaseNode {
type: 'Nth';
value: string;
}
export type SimpleSelector =
| TypeSelector
| IdSelector
| ClassSelector
| AttributeSelector
| PseudoElementSelector
| PseudoClassSelector
| Percentage
| Nth
| NestingSelector;
export interface Combinator extends BaseNode {
type: 'Combinator';
name: string;
}
export interface Block extends BaseNode {
type: 'Block';
children: Array<Declaration | Rule | Atrule>;
}
export interface Declaration extends BaseNode {
type: 'Declaration';
property: string;
value: string;
}
// for zimmerframe
export type Node =
| StyleSheet
| Rule
| Atrule
| SelectorList
| Block
| ComplexSelector
| RelativeSelector
| Combinator
| SimpleSelector
| Declaration;
}

@ -1,147 +0,0 @@
export interface BaseNode {
start: number;
end: number;
}
export interface StyleSheet extends BaseNode {
type: 'StyleSheet';
attributes: any[]; // TODO
children: Array<Atrule | Rule>;
content: {
start: number;
end: number;
styles: string;
};
}
export interface Atrule extends BaseNode {
type: 'Atrule';
name: string;
prelude: string;
block: Block | null;
}
export interface Rule extends BaseNode {
type: 'Rule';
prelude: SelectorList;
block: Block;
metadata: {
parent_rule: null | Rule;
has_local_selectors: boolean;
};
}
export interface SelectorList extends BaseNode {
type: 'SelectorList';
children: ComplexSelector[];
}
export interface ComplexSelector extends BaseNode {
type: 'ComplexSelector';
children: RelativeSelector[];
metadata: {
rule: null | Rule;
used: boolean;
};
}
export interface RelativeSelector extends BaseNode {
type: 'RelativeSelector';
combinator: null | Combinator;
selectors: SimpleSelector[];
metadata: {
is_global: boolean;
is_host: boolean;
is_root: boolean;
scoped: boolean;
};
}
export interface TypeSelector extends BaseNode {
type: 'TypeSelector';
name: string;
}
export interface IdSelector extends BaseNode {
type: 'IdSelector';
name: string;
}
export interface ClassSelector extends BaseNode {
type: 'ClassSelector';
name: string;
}
export interface AttributeSelector extends BaseNode {
type: 'AttributeSelector';
name: string;
matcher: string | null;
value: string | null;
flags: string | null;
}
export interface PseudoElementSelector extends BaseNode {
type: 'PseudoElementSelector';
name: string;
}
export interface PseudoClassSelector extends BaseNode {
type: 'PseudoClassSelector';
name: string;
args: SelectorList | null;
}
export interface Percentage extends BaseNode {
type: 'Percentage';
value: string;
}
export interface NestingSelector extends BaseNode {
type: 'NestingSelector';
name: '&';
}
export interface Nth extends BaseNode {
type: 'Nth';
value: string;
}
export type SimpleSelector =
| TypeSelector
| IdSelector
| ClassSelector
| AttributeSelector
| PseudoElementSelector
| PseudoClassSelector
| Percentage
| Nth
| NestingSelector;
export interface Combinator extends BaseNode {
type: 'Combinator';
name: string;
}
export interface Block extends BaseNode {
type: 'Block';
children: Array<Declaration | Rule | Atrule>;
}
export interface Declaration extends BaseNode {
type: 'Declaration';
property: string;
value: string;
}
// for zimmerframe
export type Node =
| StyleSheet
| Rule
| Atrule
| SelectorList
| Block
| ComplexSelector
| RelativeSelector
| Combinator
| SimpleSelector
| Declaration;

@ -10,7 +10,7 @@ import type { Location } from 'locate-character';
import type { SourceMap } from 'magic-string';
import type { Context } from 'zimmerframe';
import type { Scope } from '../phases/scope.js';
import * as Css from './css.js';
import type { Css } from './css.js';
import type { EachBlock, Namespace, SvelteNode, SvelteOptions } from './template.js';
/** The return value of `compile` from `svelte/compiler` */
@ -178,6 +178,12 @@ export interface CompileOptions extends ModuleCompileOptions {
* @default null
*/
cssOutputFilename?: string;
/**
* If `true`, compiles components with hot reloading support.
*
* @default false
*/
hmr?: boolean;
}
export interface ModuleCompileOptions {
@ -225,6 +231,7 @@ export type ValidatedCompileOptions = ValidatedModuleCompileOptions &
legacy: Required<Required<CompileOptions>['legacy']>;
runes: CompileOptions['runes'];
customElementOptions: SvelteOptions['customElement'];
hmr: CompileOptions['hmr'];
};
export type DeclarationKind =

@ -412,7 +412,7 @@ export function merge_with_preprocessor_map(result, options, source_name) {
* @param {string} from
* @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
const from_parts = from.split(/[/\\]/);
const to_parts = to.split(/[/\\]/);

@ -95,6 +95,8 @@ export const validate_component_options =
runes: boolean(undefined),
hmr: boolean(false),
sourcemap: validator(undefined, (input) => {
// Source maps can take on a variety of values, including string, JSON, map objects from magic-string and source-map,
// so there's no good way to check type validity here

@ -7,7 +7,8 @@ import {
/** @satisfies {Warnings} */
const css = {
'unused-selector': () => 'Unused CSS selector'
/** @param {string} name */
'css-unused-selector': (name) => `Unused CSS selector "${name}"`
};
/** @satisfies {Warnings} */
@ -300,6 +301,11 @@ export function warn(array, node, path, code, ...args) {
)
);
}
// Style nodes
if (current.type === 'StyleSheet' && current.content.comment) {
ignores.push(...current.content.comment.ignores);
}
}
if (ignores.includes(code)) return;

@ -16,6 +16,16 @@ export const TRANSITION_IN = 1;
export const TRANSITION_OUT = 1 << 1;
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 elements that require raw contents and should not have SSR comments put in them */
export const RawTextElements = ['textarea', 'script', 'style', 'title'];
@ -95,7 +105,6 @@ export const DOMBooleanAttributes = [
];
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
export const interactive_elements = new Set([

@ -1,6 +1,6 @@
import { current_component_context, untrack } from '../internal/client/runtime.js';
import { is_array } from '../internal/client/utils.js';
import { user_effect } from '../internal/index.js';
import { current_component_context, flush_sync, untrack } from './internal/client/runtime.js';
import { is_array } from './internal/client/utils.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.
@ -13,7 +13,7 @@ import { user_effect } from '../internal/index.js';
*
* https://svelte.dev/docs/svelte#onmount
* @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}
*/
export function onMount(fn) {
@ -80,8 +80,9 @@ function create_custom_event(type, detail, { bubbles = false, cancelable = false
* ```
*
* https://svelte.dev/docs/svelte#createeventdispatcher
* @deprecated Use callback props and/or the `$host()` rune instead see https://svelte-5-preview.vercel.app/docs/deprecations#createeventdispatcher
* @template {Record<string, any>} [EventMap = any]
* @returns {import('./public.js').EventDispatcher<EventMap>}
* @returns {import('./index.js').EventDispatcher<EventMap>}
*/
export function createEventDispatcher() {
const component_context = current_component_context;
@ -161,25 +162,30 @@ export function afterUpdate(fn) {
/**
* 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) {
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 {
flushSync,
mount,
hydrate,
tick,
unmount,
untrack,
unstate,
createRoot,
hasContext,
getContext,
getAllContexts,
setContext
} from '../internal/index.js';
hasContext,
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';

@ -59,6 +59,9 @@ export class SvelteComponent<
Events extends Record<string, any> = any,
Slots extends Record<string, any> = any
> {
/** The custom element version of the component. Only present if compiled with the `customElement` compiler option */
static element?: typeof HTMLElement;
[prop: string]: any;
/**
* @deprecated This constructor only exists when using the `asClassComponent` compatibility helper, which
@ -222,5 +225,5 @@ export interface EventDispatcher<EventMap extends Record<string, any>> {
): boolean;
}
export * from './main-client.js';
export * from './index-client.js';
import './ambient.js';

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

@ -0,0 +1,38 @@
import { block, branch, destroy_effect } from '../reactivity/effects.js';
import { set_should_intro } from '../render.js';
import { get } from '../runtime.js';
/**
* @template {(anchor: Comment, props: any) => any} Component
* @param {import("#client").Source<Component>} source
*/
export function hmr(source) {
/**
* @param {Comment} anchor
* @param {any} props
*/
return (anchor, props) => {
let instance = {};
/** @type {import("#client").Effect} */
let effect;
block(() => {
const component = get(source);
if (effect) {
// @ts-ignore
for (var k in instance) delete instance[k];
destroy_effect(effect);
}
effect = branch(() => {
set_should_intro(false);
Object.assign(instance, component(anchor, props));
set_should_intro(true);
});
});
return instance;
};
}

@ -2,6 +2,7 @@
import { STATE_SYMBOL } from '../constants.js';
import { untrack } from '../runtime.js';
import { get_descriptors } from '../utils.js';
/** @type {Record<string, Array<{ start: Location, end: Location, component: Function }>>} */
const boundaries = {};
@ -9,7 +10,7 @@ const boundaries = {};
const chrome_pattern = /at (?:.+ \()?(.+):(\d+):(\d+)\)?$/;
const firefox_pattern = /@(.+):(\d+):(\d+)$/;
export function get_stack() {
function get_stack() {
const stack = new Error().stack;
if (!stack) return null;
@ -91,49 +92,107 @@ export function mark_module_end() {
}
}
let add_owner_visited = new Set();
/**
*
* @param {any} object
* @param {any} owner
*/
export function add_owner(object, owner) {
untrack(() => {
add_owner_to_object(object, owner);
});
// Needed because ownership addition can invoke getters on a proxy,
// calling add_owner anew, so just keeping the set as part of
// add_owner_to_object would not be enough.
const prev = add_owner_visited;
try {
add_owner_visited = new Set(add_owner_visited);
untrack(() => {
add_owner_to_object(object, owner, add_owner_visited);
});
} finally {
add_owner_visited = prev;
}
}
/**
* @param {any} object
* @param {Function} owner
* @param {Set<any>} visited
*/
function add_owner_to_object(object, owner) {
function add_owner_to_object(object, owner, visited) {
if (visited.has(object)) return;
visited.add(object);
if (object?.[STATE_SYMBOL]?.o && !object[STATE_SYMBOL].o.has(owner)) {
object[STATE_SYMBOL].o.add(owner);
for (const key in object) {
add_owner_to_object(object[key], owner);
}
}
// Not inside previous if-block; there could be normal objects in-between
traverse_for_owners(object, (nested) => add_owner_to_object(nested, owner, visited));
}
let strip_owner_visited = new Set();
/**
* @param {any} object
*/
export function strip_owner(object) {
untrack(() => {
strip_owner_from_object(object);
});
// Needed because ownership stripping can invoke getters on a proxy,
// calling strip_owner anew, so just keeping the set as part of
// strip_owner_from_object would not be enough.
const prev = strip_owner_visited;
try {
untrack(() => {
strip_owner_from_object(object, strip_owner_visited);
});
} finally {
strip_owner_visited = prev;
}
}
/**
* @param {any} object
* @param {Set<any>} visited
*/
function strip_owner_from_object(object) {
function strip_owner_from_object(object, visited) {
if (visited.has(object)) return;
visited.add(object);
if (object?.[STATE_SYMBOL]?.o) {
object[STATE_SYMBOL].o = null;
}
// Not inside previous if-block; there could be normal objects in-between
traverse_for_owners(object, (nested) => strip_owner_from_object(nested, visited));
}
/**
* @param {any} object
* @param {(obj: any) => void} cb
*/
function traverse_for_owners(object, cb) {
if (typeof object === 'object' && object !== null && !(object instanceof EventTarget)) {
for (const key in object) {
strip_owner(object[key]);
cb(object[key]);
}
// deal with state on classes
const proto = Object.getPrototypeOf(object);
if (
proto !== Object.prototype &&
proto !== Array.prototype &&
proto !== Map.prototype &&
proto !== Set.prototype &&
proto !== Date.prototype
) {
const descriptors = get_descriptors(proto);
for (let key in descriptors) {
const get = descriptors[key].get;
if (get) {
try {
cb(object[key]);
} catch (e) {
// continue
}
}
}
}
}
}

@ -1,14 +1,12 @@
import { is_promise } from '../../../common.js';
import { hydrate_block_anchor } from '../hydration.js';
import { remove } from '../reconciler.js';
import { is_promise } from '../../../shared/utils.js';
import {
current_component_context,
flushSync,
flush_sync,
set_current_component_context,
set_current_effect,
set_current_reaction
} 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 { INERT } from '../../constants.js';
/**
@ -23,8 +21,6 @@ import { INERT } from '../../constants.js';
export function await_block(anchor, get_input, pending_fn, then_fn, catch_fn) {
const component_context = current_component_context;
hydrate_block_anchor(anchor);
/** @type {any} */
let input;
@ -42,22 +38,22 @@ export function await_block(anchor, get_input, pending_fn, then_fn, catch_fn) {
* @param {any} value
*/
function create_effect(fn, value) {
set_current_effect(branch);
set_current_reaction(branch); // TODO do we need both?
set_current_effect(effect);
set_current_reaction(effect); // TODO do we need both?
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_reaction(null);
set_current_effect(null);
// without this, the DOM does not update until two ticks after the promise,
// resolves which is unexpected behaviour (and somewhat irksome to test)
flushSync();
flush_sync();
return effect;
return e;
}
const branch = render_effect(() => {
const effect = block(() => {
if (input === (input = get_input())) return;
if (is_promise(input)) {
@ -65,11 +61,10 @@ export function await_block(anchor, get_input, pending_fn, then_fn, catch_fn) {
if (pending_fn) {
if (pending_effect && (pending_effect.f & INERT) === 0) {
if (pending_effect.dom) remove(pending_effect.dom);
destroy_effect(pending_effect);
}
pending_effect = render_effect(() => pending_fn(anchor), true);
pending_effect = branch(() => pending_fn(anchor));
}
if (then_effect) pause_effect(then_effect);
@ -99,19 +94,11 @@ export function await_block(anchor, get_input, pending_fn, then_fn, catch_fn) {
if (then_fn) {
if (then_effect) {
if (then_effect.dom) remove(then_effect.dom);
destroy_effect(then_effect);
}
then_effect = render_effect(() => then_fn(anchor, input), true);
then_effect = branch(() => then_fn(anchor, input));
}
}
});
branch.ondestroy = () => {
// TODO this sucks, tidy it up
if (pending_effect?.dom) remove(pending_effect.dom);
if (then_effect?.dom) remove(then_effect.dom);
if (catch_effect?.dom) remove(catch_effect.dom);
};
}

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

@ -4,33 +4,28 @@ import {
EACH_IS_CONTROLLED,
EACH_IS_STRICT_EQUALS,
EACH_ITEM_REACTIVE,
EACH_KEYED
EACH_KEYED,
HYDRATION_END_ELSE,
HYDRATION_START
} from '../../../../constants.js';
import {
hydrate_nodes,
hydrate_block_anchor,
hydrating,
set_hydrating,
update_hydrate_nodes
} from '../hydration.js';
import { hydrate_anchor, hydrate_nodes, hydrating, set_hydrating } from '../hydration.js';
import { empty } from '../operations.js';
import { insert, remove } from '../reconciler.js';
import { remove } from '../reconciler.js';
import { untrack } from '../../runtime.js';
import {
block,
branch,
destroy_effect,
effect,
run_out_transitions,
pause_children,
pause_effect,
pause_effects,
render_effect,
resume_effect,
user_effect
resume_effect
} from '../../reactivity/effects.js';
import { source, mutable_source, set } from '../../reactivity/sources.js';
import { is_array, is_frozen, map_get, map_set } from '../../utils.js';
import { STATE_SYMBOL } from '../../constants.js';
var NEW_ITEM = -1;
var LIS_ITEM = -2;
/**
* The row of a keyed each block that is currently updating. We track this
* so that `animate:` directives have something to attach themselves to
@ -43,33 +38,77 @@ export function set_current_each_item(item) {
current_each_item = item;
}
/**
* @param {any} _
* @param {number} i
*/
export function index(_, i) {
return i;
}
/**
* Pause multiple effects simultaneously, and coordinate their
* subsequent destruction. Used in each blocks
* @param {import('#client').EachItem[]} items
* @param {null | Node} controlled_anchor
* @param {() => void} [callback]
*/
function pause_effects(items, controlled_anchor, callback) {
/** @type {import('#client').TransitionManager[]} */
var transitions = [];
var length = items.length;
for (var i = 0; i < length; i++) {
pause_children(items[i].e, 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 (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(items[i].e);
}
if (callback !== undefined) callback();
});
}
/**
* @template V
* @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 {null | ((item: V) => string)} get_key
* @param {(anchor: null, item: V, index: import('#client').MaybeSource<number>) => void} render_fn
* @param {null | ((anchor: Node | null) => void)} fallback_fn
* @param {typeof reconcile_indexed_array | reconcile_tracked_array} reconcile_fn
* @param {() => V[]} get_collection
* @param {(value: V, index: number) => any} get_key
* @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}
*/
function each(anchor, get_collection, flags, get_key, render_fn, fallback_fn, reconcile_fn) {
export function each(anchor, flags, get_collection, get_key, render_fn, fallback_fn = null) {
/** @type {import('#client').EachState} */
var state = { flags, items: [] };
var state = { flags, items: new Map(), next: null };
var is_controlled = (flags & EACH_IS_CONTROLLED) !== 0;
hydrate_block_anchor(is_controlled ? /** @type {Node} */ (anchor.firstChild) : anchor);
if (is_controlled) {
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} */
var fallback = null;
var effect = render_effect(() => {
block(() => {
var collection = get_collection();
var array = is_array(collection)
@ -78,8 +117,6 @@ function each(anchor, get_collection, flags, get_key, render_fn, fallback_fn, re
? []
: Array.from(collection);
var keys = get_key === null ? array : array.map(get_key);
var length = array.length;
// If we are working with an array that isn't proxied or frozen, then remove strict equality and ensure the items
@ -98,31 +135,32 @@ function each(anchor, get_collection, flags, get_key, render_fn, fallback_fn, re
let mismatch = false;
if (hydrating) {
var is_else = /** @type {Comment} */ (hydrate_nodes?.[0])?.data === 'ssr:each_else';
var is_else = /** @type {Comment} */ (anchor).data === HYDRATION_END_ELSE;
if (is_else !== (length === 0)) {
// hydration mismatch — remove the server-rendered DOM and start over
remove(hydrate_nodes);
set_hydrating(false);
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[]} */ (hydrate_nodes).shift();
}
}
// this is separate to the previous block because `hydrating` might change
if (hydrating) {
var b_items = [];
/** @type {Node} */
var child_anchor = hydrate_nodes[0];
// Hydrate block
var hydration_list = /** @type {import('#client').TemplateNode[]} */ (hydrate_nodes);
var hydrating_node = hydration_list[0];
/** @type {import('#client').EachItem | import('#client').EachState} */
var prev = state;
for (var i = 0; i < length; i++) {
var nodes = update_hydrate_nodes(hydrating_node);
/** @type {import('#client').EachItem} */
var item;
if (nodes === null) {
for (var i = 0; i < length; i++) {
if (
child_anchor.nodeType !== 8 ||
/** @type {Comment} */ (child_anchor).data !== HYDRATION_START
) {
// 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;
@ -130,24 +168,28 @@ function each(anchor, get_collection, flags, get_key, render_fn, fallback_fn, re
break;
}
b_items[i] = create_item(array[i], keys?.[i], i, render_fn, flags);
child_anchor = hydrate_anchor(child_anchor);
var value = array[i];
var key = get_key(value, i);
item = create_item(child_anchor, prev, null, value, key, i, render_fn, flags);
state.items.set(key, item);
child_anchor = /** @type {Comment} */ (child_anchor.nextSibling);
// TODO helperise this
hydrating_node = /** @type {import('#client').TemplateNode} */ (
/** @type {Node} */ (
/** @type {Node} */ (nodes[nodes.length - 1] || hydrating_node).nextSibling
).nextSibling
);
prev = item;
}
remove_excess_hydration_nodes(hydration_list, hydrating_node);
state.items = b_items;
// 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;
}
}
}
if (!hydrating) {
// TODO add 'empty controlled block' optimisation here
reconcile_fn(array, state, anchor, render_fn, flags, keys);
reconcile(array, state, anchor, render_fn, flags, get_key);
}
if (fallback_fn !== null) {
@ -155,15 +197,7 @@ function each(anchor, get_collection, flags, get_key, render_fn, fallback_fn, re
if (fallback) {
resume_effect(fallback);
} else {
fallback = render_effect(() => {
var dom = fallback_fn(hydrating ? null : anchor);
return () => {
if (dom !== undefined) {
remove(dom);
}
};
}, true);
fallback = branch(() => fallback_fn(anchor));
}
} else if (fallback !== null) {
pause_effect(fallback, () => {
@ -177,44 +211,6 @@ function each(anchor, get_collection, flags, get_key, render_fn, fallback_fn, re
set_hydrating(true);
}
});
effect.ondestroy = () => {
for (var item of state.items) {
if (item.e.dom !== null) {
remove(item.e.dom);
destroy_effect(item.e);
}
}
if (fallback) destroy_effect(fallback);
};
}
/**
* @template V
* @param {Element | Comment} anchor
* @param {() => V[]} get_collection
* @param {number} flags
* @param {null | ((item: V) => string)} get_key
* @param {(anchor: null, item: V, index: import('#client').MaybeSource<number>) => void} render_fn
* @param {null | ((anchor: Node | null) => void)} fallback_fn
* @returns {void}
*/
export function each_keyed(anchor, get_collection, flags, get_key, render_fn, fallback_fn) {
each(anchor, get_collection, flags, get_key, render_fn, fallback_fn, reconcile_tracked_array);
}
/**
* @template V
* @param {Element | Comment} anchor
* @param {() => V[]} get_collection
* @param {number} flags
* @param {(anchor: null, item: V, index: import('#client').MaybeSource<number>) => void} render_fn
* @param {null | ((anchor: Node | null) => void)} fallback_fn
* @returns {void}
*/
export function each_indexed(anchor, get_collection, flags, render_fn, fallback_fn) {
each(anchor, get_collection, flags, null, render_fn, fallback_fn, reconcile_indexed_array);
}
/**
@ -222,321 +218,187 @@ export function each_indexed(anchor, get_collection, flags, render_fn, fallback_
* @param {Array<V>} array
* @param {import('#client').EachState} state
* @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 {(value: V, index: number) => any} get_key
* @returns {void}
*/
function reconcile_indexed_array(array, state, anchor, render_fn, flags) {
var a_items = state.items;
function reconcile(array, state, anchor, render_fn, flags, get_key) {
var is_animated = (flags & EACH_IS_ANIMATED) !== 0;
var should_update = (flags & (EACH_ITEM_REACTIVE | EACH_INDEX_REACTIVE)) !== 0;
var a = a_items.length;
var b = array.length;
var min = Math.min(a, b);
var length = array.length;
var items = state.items;
var first = state.next;
var current = first;
/** @type {typeof a_items} */
var b_items = Array(b);
/** @type {Set<import('#client').EachItem>} */
var seen = new Set();
var item;
var value;
/** @type {import('#client').EachState | import('#client').EachItem} */
var prev = state;
// update items
for (var i = 0; i < min; i += 1) {
value = array[i];
item = a_items[i];
b_items[i] = item;
update_item(item, value, i, flags);
resume_effect(item.e);
}
if (b > a) {
// add items
for (; i < b; i += 1) {
value = array[i];
item = create_item(value, null, i, render_fn, flags);
b_items[i] = item;
insert_item(item, anchor);
}
state.items = b_items;
} else if (a > b) {
// remove items
var effects = [];
for (i = b; i < a; i += 1) {
effects.push(a_items[i].e);
}
/** @type {import('#client').EachItem[]} */
var to_animate = [];
pause_effects(effects, () => {
state.items.length = b;
});
}
}
/** @type {import('#client').EachItem[]} */
var matched = [];
/**
* Reconcile arrays by the equality of the elements in the array. This algorithm
* is based on Ivi's reconcilation logic:
* https://github.com/localvoid/ivi/blob/9f1bd0918f487da5b131941228604763c5d8ef56/packages/ivi/src/client/core.ts#L968
* @template V
* @param {Array<V>} array
* @param {import('#client').EachState} state
* @param {Element | Comment | Text} anchor
* @param {(anchor: null, item: V, index: number | import('#client').Source<number>) => void} render_fn
* @param {number} flags
* @param {any[]} keys
* @returns {void}
*/
function reconcile_tracked_array(array, state, anchor, render_fn, flags, keys) {
var a_items = state.items;
/** @type {import('#client').EachItem[]} */
var stashed = [];
var a = a_items.length;
var b = array.length;
/** @type {V} */
var value;
/** @type {Array<import('#client').EachItem>} */
var b_items = Array(b);
/** @type {any} */
var key;
var is_animated = (flags & EACH_IS_ANIMATED) !== 0;
var should_update = (flags & (EACH_ITEM_REACTIVE | EACH_INDEX_REACTIVE)) !== 0;
var start = 0;
/** @type {import('#client').EachItem | undefined} */
var item;
/** @type {import('#client').Effect[]} */
var to_destroy = [];
// Step 1 — trim common suffix
while (a > 0 && b > 0 && a_items[a - 1].k === keys[b - 1]) {
item = b_items[--b] = a_items[--a];
anchor = get_first_child(item);
/** @type {number} */
var i;
resume_effect(item.e);
if (is_animated) {
for (i = 0; i < length; i += 1) {
value = array[i];
key = get_key(value, i);
item = items.get(key);
if (should_update) {
update_item(item, array[b], b, flags);
if (item !== undefined) {
item.a?.measure();
to_animate.push(item);
}
}
}
// Step 2 — trim common prefix
while (start < a && start < b && a_items[start].k === keys[start]) {
item = b_items[start] = a_items[start];
resume_effect(item.e);
for (i = 0; i < length; i += 1) {
value = array[i];
key = get_key(value, i);
item = items.get(key);
if (item === undefined) {
prev = create_item(
current ? get_first_child(current) : anchor,
prev,
prev.next,
value,
key,
i,
render_fn,
flags
);
items.set(key, prev);
matched = [];
stashed = [];
current = prev.next;
continue;
}
if (should_update) {
update_item(item, array[start], start, flags);
update_item(item, value, i, flags);
}
start += 1;
}
resume_effect(item.e);
// Step 3 — update
if (start === a) {
// add only
while (start < b) {
item = create_item(array[start], keys[start], start, render_fn, flags);
b_items[start++] = item;
insert_item(item, anchor);
}
} else if (start === b) {
// remove only
while (start < a) {
to_destroy.push(a_items[start++].e);
}
} else {
// reconcile
var moved = false;
var sources = new Int32Array(b - start);
var indexes = new Map();
var i;
var index;
var last_item;
var last_sibling;
// store the indexes of each item in the new world
for (i = start; i < b; i += 1) {
sources[i - start] = NEW_ITEM;
map_set(indexes, keys[i], i);
}
if (item !== current) {
if (seen.has(item)) {
if (matched.length < stashed.length) {
// more efficient to move later items to the front
var start = stashed[0];
var local_anchor = get_first_child(start);
var j;
/** @type {Array<import('#client').EachItem>} */
var to_animate = [];
if (is_animated) {
// for all items that were in both the old and the new list,
// measure them and store them in `to_animate` so we can
// apply animations once the DOM has been updated
for (i = 0; i < a_items.length; i += 1) {
item = a_items[i];
if (indexes.has(item.k)) {
item.a?.measure();
to_animate.push(item);
}
}
}
prev = start.prev;
// populate the `sources` array for each old item with
// its new index, so that we can calculate moves
for (i = start; i < a; i += 1) {
item = a_items[i];
index = map_get(indexes, item.k);
var a = matched[0];
var b = matched[matched.length - 1];
resume_effect(item.e);
link(a.prev, b.next);
link(prev, a);
link(b, start);
if (index === undefined) {
to_destroy.push(item.e);
} else {
moved = true;
sources[index - start] = i;
b_items[index] = item;
for (j = 0; j < matched.length; j += 1) {
move(matched[j], local_anchor);
}
if (is_animated) {
to_animate.push(item);
}
}
}
for (j = 0; j < stashed.length; j += 1) {
seen.delete(stashed[j]);
}
// 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
// I fully understand this part)
if (moved) {
mark_lis(sources);
}
current = start;
prev = b;
i -= 1;
matched = [];
stashed = [];
} else {
// more efficient to move earlier items to the back
seen.delete(item);
move(item, current ? get_first_child(current) : anchor);
// working from the back, insert new or moved items
while (b-- > start) {
index = sources[b - start];
var insert = index === NEW_ITEM;
if (insert) {
item = create_item(array[b], keys[b], b, render_fn, flags);
} else {
item = b_items[b];
if (should_update) {
update_item(item, array[b], b, flags);
link(item.prev, item.next);
link(item, prev.next);
link(prev, item);
prev = item;
}
continue;
}
if (insert || (moved && index !== LIS_ITEM)) {
last_sibling = last_item === undefined ? anchor : get_first_child(last_item);
anchor = insert_item(item, last_sibling);
matched = [];
stashed = [];
while (current !== null && current.k !== key) {
seen.add(current);
stashed.push(current);
current = current.next;
}
last_item = b_items[b] = item;
}
if (current === null) {
continue;
}
if (to_animate.length > 0) {
// TODO we need to briefly take any outroing elements out of the flow, so that
// we can figure out the eventual destination of the animating elements
// - https://github.com/sveltejs/svelte/pull/10798#issuecomment-2013681778
// - https://svelte.dev/repl/6e891305e9644a7ca7065fa95c79d2d2?version=4.2.9
user_effect(() => {
untrack(() => {
for (item of to_animate) {
item.a?.apply();
}
});
});
item = current;
}
}
pause_effects(to_destroy, () => {
state.items = b_items;
});
}
/**
* 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));
matched.push(item);
prev = item;
current = item.next;
}
}
/**
* Longest Increased Subsequence algorithm
* @param {Int32Array} a
* @returns {void}
*/
function mark_lis(a) {
var length = a.length;
var parent = new Int32Array(length);
var index = new Int32Array(length);
var index_length = 0;
var i = 0;
/** @type {number} */
var j;
/** @type {number} */
var k;
/** @type {number} */
var lo;
/** @type {number} */
var hi;
const to_destroy = Array.from(seen);
// Skip -1 values at the start of the input array `a`.
for (; a[i] === NEW_ITEM; ++i) {
/**/
while (current) {
to_destroy.push(current);
current = current.next;
}
index[0] = i++;
var controlled_anchor = (flags & EACH_IS_CONTROLLED) !== 0 && length === 0 ? anchor : null;
for (; i < length; ++i) {
k = a[i];
if (k !== NEW_ITEM) {
// Ignore -1 values.
j = index[index_length];
if (a[j] < k) {
parent[i] = j;
index[++index_length] = i;
} else {
lo = 0;
hi = index_length;
while (lo < hi) {
j = (lo + hi) >> 1;
if (a[index[j]] < k) {
lo = j + 1;
} else {
hi = j;
}
}
if (k < a[index[lo]]) {
if (lo > 0) {
parent[i] = index[lo - 1];
}
index[lo] = i;
}
}
pause_effects(to_destroy, controlled_anchor, () => {
for (var i = 0; i < to_destroy.length; i += 1) {
var item = to_destroy[i];
items.delete(item.k);
link(item.prev, item.next);
}
}
// Mutate input array `a` and assign -2 value to all nodes that are part of LIS.
j = index[index_length];
});
while (index_length-- >= 0) {
a[j] = LIS_ITEM;
j = parent[j];
if (is_animated) {
effect(() => {
untrack(() => {
for (item of to_animate) {
item.a?.apply();
}
});
});
}
}
/**
* @param {import('#client').EachItem} item
* @param {Text | Element | Comment} anchor
* @returns {Text | Element | Comment}
*/
function insert_item(item, anchor) {
var current = /** @type {import('#client').Dom} */ (item.e.dom);
return insert(current, anchor);
}
/**
* @param {import('#client').EachItem} item
* @returns {Text | Element | Comment}
@ -572,51 +434,74 @@ function update_item(item, value, index, type) {
/**
* @template V
* @param {Node} anchor
* @param {import('#client').EachItem | import('#client').EachState} prev
* @param {import('#client').EachItem | null} next
* @param {V} value
* @param {unknown} key
* @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
* @returns {import('#client').EachItem}
*/
function create_item(value, key, index, render_fn, flags) {
var each_item_not_reactive = (flags & EACH_ITEM_REACTIVE) === 0;
/** @type {import('#client').EachItem} */
var item = {
a: null,
// dom
// @ts-expect-error
e: null,
// index
i: (flags & EACH_INDEX_REACTIVE) === 0 ? index : source(index),
// key
k: key,
// item
v: each_item_not_reactive
? value
: (flags & EACH_IS_STRICT_EQUALS) !== 0
? source(value)
: mutable_source(value)
};
function create_item(anchor, prev, next, value, key, index, render_fn, flags) {
var previous_each_item = current_each_item;
try {
current_each_item = item;
var reactive = (flags & EACH_ITEM_REACTIVE) !== 0;
var mutable = (flags & EACH_IS_STRICT_EQUALS) === 0;
var v = reactive ? (mutable ? mutable_source(value) : source(value)) : value;
var i = (flags & EACH_INDEX_REACTIVE) === 0 ? index : source(index);
/** @type {import('#client').EachItem} */
var item = {
i,
v,
k: key,
a: null,
// @ts-expect-error
e: null,
prev,
next
};
prev.next = item;
if (next !== null) next.prev = item;
item.e = render_effect(() => {
var dom = render_fn(null, item.v, item.i);
return () => {
if (dom !== undefined) {
remove(dom);
}
};
}, true);
current_each_item = item;
item.e = branch(() => render_fn(anchor, v, i));
return item;
} finally {
current_each_item = previous_each_item;
}
}
/**
* @param {import('#client').EachItem} item
* @param {Text | Element | Comment} anchor
*/
function move(item, anchor) {
var dom = item.e.dom;
if (dom !== null) {
if (is_array(dom)) {
for (var i = 0; i < dom.length; i++) {
anchor.before(dom[i]);
}
} else {
anchor.before(dom);
}
}
}
/**
*
* @param {import('#client').EachItem | import('#client').EachState} prev
* @param {import('#client').EachItem | null} next
*/
function link(prev, next) {
prev.next = next;
if (next !== null) next.prev = prev;
}

@ -1,29 +1,66 @@
import { derived } from '../../reactivity/deriveds.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 {boolean} svg
* @returns {void}
*/
export function html(dom, get_value, svg) {
/** @type {import('#client').Dom} */
let html_dom;
export function html(anchor, get_value, svg) {
let value = derived(get_value);
/** @type {string} */
let value;
render_effect(() => {
var dom = html_to_dom(anchor, get(value), svg);
const effect = render_effect(() => {
if (value !== (value = get_value())) {
if (html_dom) remove(html_dom);
html_dom = reconcile_html(dom, value, svg);
if (dom) {
return () => remove(dom);
}
});
}
/**
* 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>`;
effect.ondestroy = () => {
if (html_dom) {
remove(html_dom);
// Don't use create_fragment_with_script_from_html here because that would mean script tags are executed.
// @html is basically `.innerHTML = ...` and that doesn't execute scripts either due to security reasons.
/** @type {DocumentFragment | Element} */
var node = create_fragment_from_html(html);
if (svg) {
node = /** @type {Element} */ (node.firstChild);
}
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;
}

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

Loading…
Cancel
Save