Merge branch 'main' into snippet-work

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -21,15 +21,18 @@
"big-eggs-flash",
"big-eyes-carry",
"big-geese-act",
"big-moons-occur",
"blue-ants-raise",
"blue-rules-juggle",
"blue-timers-film",
"brave-points-sleep",
"brave-shrimps-kiss",
"brave-walls-destroy",
"brave-walls-flow",
"breezy-carrots-flash",
"bright-peas-juggle",
"bright-snakes-sing",
"brown-houses-obey",
"brown-months-fry",
"brown-spoons-boil",
"calm-ravens-sneeze",
@ -46,12 +49,14 @@
"cold-birds-own",
"cold-masks-learn",
"cool-ants-leave",
"cool-peas-lick",
"cool-rabbits-tickle",
"cool-roses-trade",
"cuddly-pianos-drop",
"curly-lizards-dream",
"curvy-buses-laugh",
"curvy-cups-cough",
"curvy-flies-exercise",
"curvy-ties-shout",
"cyan-flowers-destroy",
"cyan-spies-grin",
@ -62,6 +67,7 @@
"dry-clocks-grow",
"dry-eggs-play",
"dry-eggs-retire",
"dry-pillows-exist",
"dull-coins-vanish",
"dull-mangos-wave",
"dull-pots-add",
@ -70,6 +76,7 @@
"eight-steaks-shout",
"eighty-bikes-camp",
"eighty-days-cheat",
"eleven-beers-yell",
"eleven-cycles-applaud",
"empty-bags-heal",
"empty-bulldogs-exercise",
@ -82,6 +89,7 @@
"famous-knives-sneeze",
"famous-pants-pay",
"fast-weeks-clean",
"few-clouds-shop",
"few-mugs-fail",
"fifty-rice-wait",
"fifty-steaks-float",
@ -101,6 +109,7 @@
"friendly-lies-camp",
"funny-wombats-argue",
"fuzzy-bags-camp",
"fuzzy-donuts-provide",
"gentle-dolls-juggle",
"gentle-sheep-hug",
"gentle-spies-happen",
@ -117,6 +126,7 @@
"green-eggs-approve",
"green-hounds-play",
"green-tigers-judge",
"grumpy-jars-sparkle",
"happy-beds-scream",
"happy-suits-film",
"healthy-planes-vanish",
@ -126,12 +136,14 @@
"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,6 +160,7 @@
"large-clouds-carry",
"large-turkeys-deny",
"late-crabs-lay",
"late-peaches-mate",
"lazy-masks-sit",
"lazy-months-knock",
"lazy-spiders-think",
@ -159,15 +172,21 @@
"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",
"moody-carrots-lay",
@ -179,8 +198,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",
@ -223,8 +244,10 @@
"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-olives-yell",
"rich-sheep-burn",
@ -239,8 +262,10 @@
"selfish-dragons-knock",
"selfish-spies-help",
"selfish-tools-hide",
"serious-gorillas-eat",
"serious-kids-deliver",
"serious-needles-joke",
"serious-poems-brake",
"serious-socks-cover",
"serious-zebras-scream",
"seven-deers-jam",
@ -258,6 +283,8 @@
"short-countries-rush",
"silent-apes-report",
"silly-laws-happen",
"silly-lies-film",
"silly-ways-wash",
"silver-points-approve",
"sixty-items-crash",
"slimy-clouds-talk",
@ -266,9 +293,12 @@
"slow-beds-shave",
"slow-chefs-dream",
"slow-kids-sparkle",
"slow-plums-chew",
"slow-wombats-reply",
"small-papayas-laugh",
"small-sheep-type",
"small-spiders-fail",
"smart-cherries-leave",
"smart-parents-swim",
"smart-turkeys-tell",
"smart-zebras-pay",
@ -290,6 +320,7 @@
"spotty-turkeys-sparkle",
"stale-books-perform",
"stale-comics-look",
"stale-fans-rest",
"stale-jeans-refuse",
"strange-apricots-happen",
"strong-gifts-smoke",
@ -300,6 +331,7 @@
"swift-donkeys-perform",
"swift-fans-stare",
"swift-feet-juggle",
"swift-poets-carry",
"swift-ravens-hunt",
"swift-seahorses-deliver",
"tall-books-grin",
@ -316,6 +348,7 @@
"ten-foxes-repeat",
"ten-jokes-divide",
"ten-peaches-sleep",
"ten-singers-cough",
"ten-ties-repair",
"ten-worms-reflect",
"thick-cycles-rule",
@ -328,7 +361,9 @@
"thirty-pears-hug",
"thirty-wombats-relax",
"three-camels-sell",
"three-foxes-relax",
"three-icons-trade",
"three-lions-visit",
"three-papayas-buy",
"three-suits-grin",
"tidy-buses-whisper",
@ -351,6 +386,7 @@
"wicked-doors-train",
"wicked-hairs-cheer",
"wild-foxes-wonder",
"wild-moose-compare",
"wise-apples-care",
"wise-dancers-hang",
"wise-dodos-tell",

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -11,6 +11,7 @@
**/vite.config.js
**/vite.prod.config.js
**/node_modules
**/compiler/index.js
**/tests/**
@ -20,4 +21,4 @@ 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/**
sites/svelte.dev/**

@ -10,12 +10,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

@ -37,7 +37,7 @@
"eslint": "^8.56.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",

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

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

@ -2,7 +2,7 @@
"name": "svelte",
"description": "Cybernetically enhanced web apps",
"license": "MIT",
"version": "5.0.0-next.81",
"version": "5.0.0-next.93",
"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",
@ -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('current_hydration_fragment')) {
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`,

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

@ -182,6 +182,7 @@ const runes = {
`$props() assignment must not contain nested properties or computed keys`,
'invalid-props-location': () =>
`$props() can only be used at the top level of components as a variable declaration initializer`,
'invalid-bindable-location': () => `$bindable() can only be used inside a $props() declaration`,
/** @param {string} rune */
'invalid-state-location': (rune) =>
`${rune}(...) can only be used as a variable declaration initializer or a class field`,
@ -281,7 +282,9 @@ const attributes = {
},
'invalid-let-directive-placement': () => 'let directive at invalid position',
'invalid-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;

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

@ -258,10 +258,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 +397,8 @@ export function analyze_component(root, options) {
})
: '',
keyframes: []
}
},
source
};
if (!options.customElement && root.options?.customElement) {
@ -436,7 +438,7 @@ export function analyze_component(root, options) {
);
}
} else {
instance.scope.declare(b.id('$$props'), 'prop', 'synthetic');
instance.scope.declare(b.id('$$props'), 'bindable_prop', 'synthetic');
instance.scope.declare(b.id('$$restProps'), 'rest_prop', 'synthetic');
for (const { ast, scope, scopes } of [module, instance, template]) {
@ -466,7 +468,10 @@ export function analyze_component(root, options) {
}
for (const [name, binding] of instance.scope.declarations) {
if (binding.kind === 'prop' && binding.node.name !== '$$props') {
if (
(binding.kind === 'prop' || binding.kind === 'bindable_prop') &&
binding.node.name !== '$$props'
) {
const references = binding.references.filter(
(r) => r.node !== binding.node && r.path.at(-1)?.type !== 'ExportSpecifier'
);
@ -605,20 +610,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;
}
}
@ -627,8 +650,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'
)
) {
@ -657,15 +680,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);
}
}
}
@ -753,12 +790,13 @@ const legacy_scope_tweaker = {
state.scope.get(specifier.local.name)
);
if (
binding.kind === 'state' ||
binding.kind === 'frozen_state' ||
(binding.kind === 'normal' &&
(binding.declaration_kind === 'let' || binding.declaration_kind === 'var'))
binding !== null &&
(binding.kind === 'state' ||
binding.kind === 'frozen_state' ||
(binding.kind === 'normal' &&
(binding.declaration_kind === 'let' || binding.declaration_kind === 'var')))
) {
binding.kind = 'prop';
binding.kind = 'bindable_prop';
if (specifier.exported.name !== specifier.local.name) {
binding.prop_alias = specifier.exported.name;
}
@ -796,7 +834,7 @@ const legacy_scope_tweaker = {
for (const declarator of node.declaration.declarations) {
for (const id of extract_identifiers(declarator.id)) {
const binding = /** @type {import('#compiler').Binding} */ (state.scope.get(id.name));
binding.kind = 'prop';
binding.kind = 'bindable_prop';
}
}
}
@ -885,11 +923,24 @@ const runes_scope_tweaker = {
property.key.type === 'Identifier'
? property.key.name
: /** @type {string} */ (/** @type {import('estree').Literal} */ (property.key).value);
const initial = property.value.type === 'AssignmentPattern' ? property.value.right : null;
let initial = property.value.type === 'AssignmentPattern' ? property.value.right : null;
const binding = /** @type {import('#compiler').Binding} */ (state.scope.get(name));
binding.prop_alias = alias;
binding.initial = initial; // rewire initial from $props() to the actual initial value
// rewire initial from $props() to the actual initial value, stripping $bindable() if necessary
if (
initial?.type === 'CallExpression' &&
initial.callee.type === 'Identifier' &&
initial.callee.name === '$bindable'
) {
binding.initial = /** @type {import('estree').Expression | null} */ (
initial.arguments[0] ?? null
);
binding.kind = 'bindable_prop';
} else {
binding.initial = initial;
}
}
}
},
@ -935,25 +986,6 @@ const runes_scope_tweaker = {
function is_known_safe_call(node, context) {
const callee = node.callee;
// Check for selector() API calls
if (callee.type === 'MemberExpression' && callee.object.type === 'Identifier') {
const binding = context.state.scope.get(callee.object.name);
const selector_binding = context.state.scope.get('selector');
if (
selector_binding !== null &&
selector_binding.declaration_kind === 'import' &&
selector_binding.initial !== null &&
selector_binding.initial.type === 'ImportDeclaration' &&
selector_binding.initial.source.value === 'svelte' &&
binding !== null &&
binding.initial !== null &&
binding.initial.type === 'CallExpression' &&
binding.initial.callee.type === 'Identifier' &&
binding.initial.callee.name === 'selector'
) {
return true;
}
}
// String / Number / BigInt / Boolean casting calls
if (callee.type === 'Identifier') {
const name = callee.name;
@ -1345,21 +1377,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]);
}
}
}
@ -1383,14 +1415,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,21 @@ 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') {
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');
}
@ -299,17 +320,19 @@ const validation = {
error(node, 'invalid-binding-expression');
}
const binding = context.state.scope.get(left.name);
if (
assignee.type === 'Identifier' &&
node.name !== 'this' // bind:this also works for regular variables
) {
const binding = context.state.scope.get(left.name);
// reassignment
if (
!binding ||
(binding.kind !== 'state' &&
binding.kind !== 'frozen_state' &&
binding.kind !== 'prop' &&
binding.kind !== 'bindable_prop' &&
binding.kind !== 'each' &&
binding.kind !== 'store_sub' &&
!binding.mutated)
@ -328,8 +351,6 @@ const validation = {
// TODO handle mutations of non-state/props in runes mode
}
const binding = context.state.scope.get(left.name);
if (node.name === 'group') {
if (!binding) {
error(node, 'INTERNAL', 'Cannot find declaration for bind:group');
@ -780,7 +801,25 @@ function validate_call_expression(node, scope, path) {
error(node, 'invalid-props-location');
}
if (rune === '$state' || rune === '$derived' || rune === '$derived.by') {
if (rune === '$bindable') {
if (parent.type === 'AssignmentPattern' && path.at(-3)?.type === 'ObjectPattern') {
const declarator = path.at(-4);
if (
declarator?.type === 'VariableDeclarator' &&
get_rune(declarator.init, scope) === '$props'
) {
return;
}
}
error(node, 'invalid-bindable-location');
}
if (
rune === '$state' ||
rune === '$state.frozen' ||
rune === '$derived' ||
rune === '$derived.by'
) {
if (parent.type === 'VariableDeclarator') return;
if (parent.type === 'PropertyDefinition' && !parent.static && !parent.computed) return;
error(node, 'invalid-state-location', rune);
@ -873,6 +912,8 @@ export const validation_runes_js = {
error(node, 'invalid-rune-args-length', rune, [0, 1]);
} else if (rune === '$props') {
error(node, 'invalid-props-location');
} else if (rune === '$bindable') {
error(node, 'invalid-bindable-location');
}
},
AssignmentExpression(node, { state }) {
@ -1022,6 +1063,9 @@ export const validation_runes = merge(validation, a11y_validators, {
}
},
CallExpression(node, { state, path }) {
if (get_rune(node, state.scope) === '$bindable' && node.arguments.length > 1) {
error(node, 'invalid-rune-args-length', '$bindable', [0, 1]);
}
validate_call_expression(node, state.scope, path);
},
EachBlock(node, { next, state }) {
@ -1062,7 +1106,7 @@ export const validation_runes = merge(validation, a11y_validators, {
state.has_props_rune = true;
if (args.length > 0) {
error(node, 'invalid-rune-args-length', '$props', [0]);
error(node, 'invalid-rune-args-length', rune, [0]);
}
if (node.id.type !== 'ObjectPattern') {

@ -46,27 +46,27 @@ 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 init() {
get before_init() {
/** @type {any[]} */
const a = [];
a.push = () => error(null, 'INTERNAL', 'init.push should not be called outside create_block');
a.push = () =>
error(null, 'INTERNAL', 'before_init.push should not be called outside create_block');
return a;
},
get update() {
get init() {
/** @type {any[]} */
const a = [];
a.push = () =>
error(null, 'INTERNAL', 'update.push should not be called outside create_block');
a.push = () => error(null, 'INTERNAL', 'init.push should not be called outside create_block');
return a;
},
get update_effects() {
get update() {
/** @type {any[]} */
const a = [];
a.push = () =>
error(null, 'INTERNAL', 'update_effects.push should not be called outside create_block');
error(null, 'INTERNAL', 'update.push should not be called outside create_block');
return a;
},
get after_update() {
@ -239,7 +239,7 @@ export function client_component(source, analysis, options) {
);
});
const properties = analysis.exports.map(({ name, alias }) => {
const component_returned_object = analysis.exports.map(({ name, alias }) => {
const expression = serialize_get_binding(b.id(name), instance_state);
if (expression.type === 'Identifier' && !options.dev) {
@ -249,17 +249,33 @@ export function client_component(source, analysis, options) {
return b.get(alias ?? name, [b.return(expression)]);
});
if (analysis.accessors) {
for (const [name, binding] of analysis.instance.scope.declarations) {
if (binding.kind !== 'prop' || name.startsWith('$$')) continue;
const properties = [...analysis.instance.scope.declarations].filter(
([name, binding]) =>
(binding.kind === 'prop' || binding.kind === 'bindable_prop') && !name.startsWith('$$')
);
if (analysis.runes && options.dev) {
/** @type {import('estree').Literal[]} */
const bindable = [];
for (const [name, binding] of properties) {
if (binding.kind === 'bindable_prop') {
bindable.push(b.literal(binding.prop_alias ?? name));
}
}
instance.body.unshift(
b.stmt(b.call('$.validate_prop_bindings', b.id('$$props'), b.array(bindable)))
);
}
if (analysis.accessors) {
for (const [name, binding] of properties) {
const key = binding.prop_alias ?? name;
const getter = b.get(key, [b.return(b.call(b.id(name)))]);
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) {
@ -271,12 +287,12 @@ export function client_component(source, analysis, options) {
};
}
properties.push(getter, setter);
component_returned_object.push(getter, setter);
}
}
if (options.legacy.componentApi) {
properties.push(
component_returned_object.push(
b.init('$set', b.id('$.update_legacy_props')),
b.init(
'$on',
@ -292,7 +308,7 @@ export function client_component(source, analysis, options) {
)
);
} else if (options.dev) {
properties.push(
component_returned_object.push(
b.init(
'$set',
b.thunk(
@ -360,8 +376,8 @@ export function client_component(source, analysis, options) {
append_styles();
component_block.body.push(
properties.length > 0
? b.return(b.call('$.pop', b.object(properties)))
component_returned_object.length > 0
? b.return(b.call('$.pop', b.object(component_returned_object)))
: b.stmt(b.call('$.pop'))
);
@ -369,7 +385,7 @@ export function client_component(source, analysis, options) {
/** @type {string[]} */
const named_props = analysis.exports.map(({ name, alias }) => alias ?? name);
for (const [name, binding] of analysis.instance.scope.declarations) {
if (binding.kind === 'prop') named_props.push(binding.prop_alias ?? name);
if (binding.kind === 'bindable_prop') named_props.push(binding.prop_alias ?? name);
}
component_block.body.unshift(
@ -476,9 +492,7 @@ export function client_component(source, analysis, options) {
/** @type {import('estree').Property[]} */
const props_str = [];
for (const [name, binding] of analysis.instance.scope.declarations) {
if (binding.kind !== 'prop' || name.startsWith('$$')) continue;
for (const [name, binding] of properties) {
const key = binding.prop_alias ?? name;
const prop_def = typeof ce === 'boolean' ? {} : ce.props?.[key] || {};
if (
@ -568,6 +582,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,18 +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 separate render effects (due to call expressions) */
readonly update_effects: Statement[];
/** Stuff that happens inside the render effect */
readonly update: {
init?: Statement;
/** If the update array only contains a single entry, this singular entry will be used, if present */
singular?: Statement;
/** Used if condition for singular prop is false (see comment above) */
grouped: Statement;
}[];
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 */

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

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

@ -40,7 +40,7 @@ export const javascript_visitors_legacy = {
state.scope.get_bindings(declarator)
);
const has_state = bindings.some((binding) => binding.kind === 'state');
const has_props = bindings.some((binding) => binding.kind === 'prop');
const has_props = bindings.some((binding) => binding.kind === 'bindable_prop');
if (!has_state && !has_props) {
const init = declarator.init;
@ -80,7 +80,7 @@ export const javascript_visitors_legacy = {
declarations.push(
b.declarator(
path.node,
binding.kind === 'prop'
binding.kind === 'bindable_prop'
? get_prop_source(binding, state, binding.prop_alias ?? name, value)
: value
)
@ -168,7 +168,7 @@ export const javascript_visitors_legacy = {
// If the binding is a prop, we need to deep read it because it could be fine-grained $state
// from a runes-component, where mutations don't trigger an update on the prop as a whole.
if (name === '$$props' || name === '$$restProps' || binding.kind === 'prop') {
if (name === '$$props' || name === '$$restProps' || binding.kind === 'bindable_prop') {
serialized = b.call('$.deep_read_state', serialized);
}

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

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

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

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

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

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

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

@ -16,6 +16,15 @@ 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 Element events that will be delegated */
export const DelegatedEvents = [
'beforeinput',
@ -92,7 +101,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) {
@ -81,7 +81,7 @@ function create_custom_event(type, detail, { bubbles = false, cancelable = false
*
* https://svelte.dev/docs/svelte#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 +161,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';

@ -222,5 +222,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');

@ -9,7 +9,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;

@ -1,16 +1,13 @@
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 { DESTROYED, INERT } from '../../constants.js';
import { create_block } from './utils.js';
import { block, branch, destroy_effect, pause_effect } from '../../reactivity/effects.js';
import { INERT } from '../../constants.js';
/**
* @template V
@ -22,12 +19,8 @@ import { create_block } from './utils.js';
* @returns {void}
*/
export function await_block(anchor, get_input, pending_fn, then_fn, catch_fn) {
const block = create_block();
const component_context = current_component_context;
hydrate_block_anchor(anchor);
/** @type {any} */
let input;
@ -45,34 +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();
return effect;
}
/** @param {import('#client').Effect} effect */
function pause(effect) {
if ((effect.f & DESTROYED) !== 0) return;
const block = effect.block;
flush_sync();
pause_effect(effect, () => {
// TODO make this unnecessary
const dom = block?.d;
if (dom) remove(dom);
});
return e;
}
const branch = render_effect(() => {
const effect = block(() => {
if (input === (input = get_input())) return;
if (is_promise(input)) {
@ -80,20 +61,19 @@ export function await_block(anchor, get_input, pending_fn, then_fn, catch_fn) {
if (pending_fn) {
if (pending_effect && (pending_effect.f & INERT) === 0) {
if (pending_effect.block?.d) remove(pending_effect.block.d);
destroy_effect(pending_effect);
}
pending_effect = render_effect(() => pending_fn(anchor), {}, true);
pending_effect = branch(() => pending_fn(anchor));
}
if (then_effect) pause(then_effect);
if (catch_effect) pause(catch_effect);
if (then_effect) pause_effect(then_effect);
if (catch_effect) pause_effect(catch_effect);
promise.then(
(value) => {
if (promise !== input) return;
if (pending_effect) pause(pending_effect);
if (pending_effect) pause_effect(pending_effect);
if (then_fn) {
then_effect = create_effect(then_fn, value);
@ -101,7 +81,7 @@ export function await_block(anchor, get_input, pending_fn, then_fn, catch_fn) {
},
(error) => {
if (promise !== input) return;
if (pending_effect) pause(pending_effect);
if (pending_effect) pause_effect(pending_effect);
if (catch_fn) {
catch_effect = create_effect(catch_fn, error);
@ -109,24 +89,16 @@ export function await_block(anchor, get_input, pending_fn, then_fn, catch_fn) {
}
);
} else {
if (pending_effect) pause(pending_effect);
if (catch_effect) pause(catch_effect);
if (pending_effect) pause_effect(pending_effect);
if (catch_effect) pause_effect(catch_effect);
if (then_fn) {
if (then_effect) {
if (then_effect.block?.d) remove(then_effect.block.d);
destroy_effect(then_effect);
}
then_effect = render_effect(() => then_fn(anchor, input), {}, true);
then_effect = branch(() => then_fn(anchor, input));
}
}
}, block);
branch.ondestroy = () => {
// TODO this sucks, tidy it up
if (pending_effect?.block?.d) remove(pending_effect.block.d);
if (then_effect?.block?.d) remove(then_effect.block.d);
if (catch_effect?.block?.d) remove(catch_effect.block.d);
};
});
}

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

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

@ -1,29 +1,66 @@
import { derived } from '../../reactivity/deriveds.js';
import { render_effect } from '../../reactivity/effects.js';
import { 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').TemplateNode | import('#client').TemplateNode[]} */
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;
}

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

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

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

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

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

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

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

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

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

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

@ -1,5 +1,5 @@
import { hydrating } from '../../hydration.js';
import { destroy_effect, managed_effect, render_effect } from '../../../reactivity/effects.js';
import { render_effect, effect } from '../../../reactivity/effects.js';
import { listen } from './shared.js';
/** @param {TimeRanges} ranges */
@ -115,48 +115,22 @@ export function bind_ready_state(media, update) {
export function bind_playback_rate(media, get_value, update) {
var updating = false;
var callback = () => {
if (!updating) {
update(media.playbackRate);
}
updating = false;
};
// Needs to happen after the element is inserted into the dom, else playback will be set back to 1 by the browser.
// For hydration we could do it immediately but the additional code is not worth the lost microtask.
effect(() => {
var value = get_value();
/** @type {import('#client').Effect | undefined} */
var render;
var destroyed = false;
var effect = managed_effect(() => {
destroy_effect(effect);
if (destroyed) return;
if (get_value() == null) {
callback();
// through isNaN we also allow number strings, which is more robust
if (!isNaN(/** @type {any} */ (value)) && value !== media.playbackRate) {
updating = true;
media.playbackRate = /** @type {number} */ (value);
}
listen(media, ['ratechange'], callback, false);
render = render_effect(() => {
var value = get_value();
// through isNaN we also allow number strings, which is more robust
if (!isNaN(/** @type {any} */ (value)) && value !== media.playbackRate) {
updating = true;
media.playbackRate = /** @type {number} */ (value);
}
listen(media, ['ratechange'], () => {
if (!updating) update(media.playbackRate);
updating = false;
});
});
render_effect(() => () => {
destroyed = true;
if (render) {
destroy_effect(render);
}
});
}
/**

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

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

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

@ -1,35 +1,12 @@
import { hydrating } from '../hydration.js';
import { set_class_name } from '../operations.js';
import { render_effect } from '../../reactivity/effects.js';
/**
* @param {HTMLElement} dom
* @param {() => string} value
* @returns {void}
*/
export function class_name_effect(dom, value) {
render_effect(() => {
class_name(dom, value());
});
}
/**
* @param {SVGElement} dom
* @param {() => string} value
* @returns {void}
*/
export function svg_class_name_effect(dom, value) {
render_effect(() => {
svg_class_name(dom, value());
});
}
/**
* @param {SVGElement} dom
* @param {string} value
* @returns {void}
*/
export function svg_class_name(dom, value) {
export function set_svg_class(dom, value) {
// @ts-expect-error need to add __className to patched prototype
var prev_class_name = dom.__className;
var next_class_name = to_class(value);
@ -58,7 +35,7 @@ export function svg_class_name(dom, value) {
* @param {string} value
* @returns {void}
*/
export function class_name(dom, value) {
export function set_class(dom, value) {
// @ts-expect-error need to add __className to patched prototype
var prev_class_name = dom.__className;
var next_class_name = to_class(value);
@ -90,7 +67,7 @@ export function class_name(dom, value) {
* @param {V} value
* @returns {string | V}
*/
export function to_class(value) {
function to_class(value) {
return value == null ? '' : value;
}
@ -100,22 +77,10 @@ export function to_class(value) {
* @param {boolean} value
* @returns {void}
*/
export function class_toggle(dom, class_name, value) {
export function toggle_class(dom, class_name, value) {
if (value) {
dom.classList.add(class_name);
} else {
dom.classList.remove(class_name);
}
}
/**
* @param {Element} dom
* @param {string} class_name
* @param {() => boolean} value
* @returns {void}
*/
export function class_toggle_effect(dom, class_name, value) {
render_effect(() => {
class_toggle(dom, class_name, value());
});
}

@ -1,8 +1,8 @@
import { createClassComponent } from '../../../../legacy/legacy-client.js';
import { destroy_effect, render_effect } from '../../reactivity/effects.js';
import { open, close } from '../template.js';
import { define_property } from '../../utils.js';
import { add_snippet_symbol } from '../blocks/snippet.js';
import { append } from '../template.js';
import { define_property, object_keys } from '../../utils.js';
/**
* @typedef {Object} CustomElementPropDefinition
@ -99,14 +99,10 @@ if (typeof HTMLElement === 'function') {
* @param {Element} anchor
*/
return (anchor) => {
const node = open(anchor, true, () => {
const slot = document.createElement('slot');
if (name !== 'default') {
slot.name = name;
}
return slot;
});
close(anchor, /** @type {Element} */ (node));
const slot = document.createElement('slot');
if (name !== 'default') slot.name = name;
append(anchor, slot);
};
}
/** @type {Record<string, any>} */
@ -150,7 +146,7 @@ if (typeof HTMLElement === 'function') {
// Reflect component props as attributes
this.$$me = render_effect(() => {
this.$$r = true;
for (const key of Object.keys(this.$$c)) {
for (const key of object_keys(this.$$c)) {
if (!this.$$p_d[key]?.reflect) continue;
this.$$d[key] = this.$$c[key];
const attribute_value = get_custom_element_value(
@ -210,7 +206,7 @@ if (typeof HTMLElement === 'function') {
*/
$$g_p(attribute_name) {
return (
Object.keys(this.$$p_d).find(
object_keys(this.$$p_d).find(
(key) =>
this.$$p_d[key].attribute === attribute_name ||
(!this.$$p_d[key].attribute && key.toLowerCase() === attribute_name)
@ -295,12 +291,12 @@ export function create_custom_element(
this.$$p_d = props_definition;
}
static get observedAttributes() {
return Object.keys(props_definition).map((key) =>
return object_keys(props_definition).map((key) =>
(props_definition[key].attribute || key).toLowerCase()
);
}
};
Object.keys(props_definition).forEach((prop) => {
object_keys(props_definition).forEach((prop) => {
define_property(Class.prototype, prop, {
get() {
return this.$$c && prop in this.$$c ? this.$$c[prop] : this.$$d[prop];

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

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

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

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

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

Loading…
Cancel
Save