Merge branch 'main' into non-recursive-mark-reactions-2

non-recursive-mark-reactions-2
Rich Harris 2 days ago committed by GitHub
commit 7ce419bb4c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: remove correct event listener from document

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: make more types from `svelte/compiler` public

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: support contenteditable binding undefined fallback

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: correctly serialize object assignment expressions

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: robustify migration script around indentation and comments

@ -0,0 +1,5 @@
---
'svelte': patch
---
feat: more accurate `render`/`mount`/`hydrate` options

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: wait a microtask for await blocks to reduce UI churn

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: ensure state update expressions are serialised correctly

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: ensure element size bindings don't unsubscribe multiple times from the resize observer

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: repair each block length even without an else

@ -0,0 +1,5 @@
---
'svelte': patch
---
breaking: prevent usage of arguments keyword in certain places

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: prevent misidentification of bindings as delegatable event handlers if used outside event attribute

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix(types): export CompileResult and Warning

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: ensure element dir properties persist with text changes

@ -0,0 +1,5 @@
---
"svelte": patch
---
breaking: bump dts-buddy

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: preserve current input values when removing defaults

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: better binding interop between runes/non-runes components

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: disallow accessing internal Svelte props

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: throw compilation error for malformed snippets

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: make media bindings more robust

@ -42,6 +42,7 @@
"breezy-carrots-flash",
"breezy-rules-beg",
"breezy-waves-camp",
"bright-berries-smell",
"bright-falcons-float",
"bright-peas-juggle",
"bright-snakes-sing",
@ -86,6 +87,7 @@
"cool-roses-trade",
"cuddly-pianos-drop",
"cuddly-points-tickle",
"curly-cooks-cheer",
"curly-lizards-dream",
"curvy-buses-laugh",
"curvy-cups-cough",
@ -107,6 +109,7 @@
"dry-eggs-play",
"dry-eggs-retire",
"dry-fans-march",
"dry-parrots-bathe",
"dry-pillows-exist",
"dull-coins-vanish",
"dull-donkeys-smell",
@ -126,6 +129,7 @@
"eighty-bikes-camp",
"eighty-days-cheat",
"eighty-lizards-notice",
"eleven-avocados-walk",
"eleven-beers-yell",
"eleven-cycles-applaud",
"eleven-hounds-pump",
@ -133,6 +137,7 @@
"empty-bulldogs-exercise",
"empty-coins-build",
"empty-crabs-think",
"empty-files-prove",
"empty-flowers-change",
"empty-geckos-pretend",
"empty-horses-tell",
@ -141,6 +146,7 @@
"fair-crabs-check",
"fair-pianos-talk",
"fair-spies-repeat",
"famous-chairs-notice",
"famous-falcons-melt",
"famous-kiwis-thank",
"famous-knives-sneeze",
@ -187,6 +193,7 @@
"fuzzy-bags-camp",
"fuzzy-donuts-provide",
"gentle-dolls-juggle",
"gentle-eagles-walk",
"gentle-sheep-hug",
"gentle-spies-happen",
"gentle-ties-fetch",
@ -205,6 +212,8 @@
"good-plums-type",
"good-rivers-yawn",
"good-roses-argue",
"gorgeous-boxes-design",
"gorgeous-hats-wonder",
"gorgeous-monkeys-carry",
"gorgeous-singers-rest",
"great-fans-unite",
@ -218,9 +227,11 @@
"grumpy-jars-sparkle",
"happy-beds-scream",
"happy-dogs-jump",
"happy-lobsters-lick",
"happy-suits-film",
"healthy-ants-film",
"healthy-planes-vanish",
"healthy-zebras-accept",
"heavy-comics-move",
"heavy-doors-applaud",
"heavy-ducks-leave",
@ -234,6 +245,7 @@
"honest-pans-kick",
"hot-cooks-repair",
"hot-jobs-tap",
"hot-rivers-punch",
"hot-sloths-clap",
"hungry-boxes-relate",
"hungry-dots-fry",
@ -303,6 +315,7 @@
"lovely-carpets-lick",
"lovely-houses-own",
"lovely-items-turn",
"lovely-ravens-crash",
"lovely-rules-eat",
"lovely-zebras-own",
"lucky-colts-remember",
@ -319,13 +332,16 @@
"mighty-cooks-scream",
"mighty-files-hammer",
"mighty-frogs-obey",
"mighty-shoes-nail",
"modern-apricots-promise",
"modern-fishes-double",
"moody-carrots-lay",
"moody-frogs-exist",
"moody-ghosts-buy",
"moody-houses-argue",
"moody-owls-cry",
"moody-sheep-type",
"moody-toys-relax",
"nasty-glasses-begin",
"nasty-lions-double",
"nasty-yaks-peel",
@ -334,6 +350,7 @@
"neat-files-rescue",
"neat-jokes-beam",
"nervous-berries-boil",
"nervous-ducks-repeat",
"nervous-spoons-relax",
"nervous-turkeys-end",
"new-boats-wait",
@ -356,6 +373,7 @@
"old-mails-sneeze",
"old-oranges-compete",
"olive-apples-lick",
"olive-cobras-wonder",
"olive-kangaroos-brake",
"olive-mice-fix",
"olive-moons-act",
@ -377,11 +395,13 @@
"polite-dolphins-care",
"polite-pumpkins-guess",
"polite-ravens-study",
"polite-ways-serve",
"poor-eggs-enjoy",
"poor-hats-design",
"poor-seahorses-flash",
"popular-apes-bathe",
"popular-cameras-tie",
"popular-feet-rule",
"popular-games-hug",
"popular-ligers-perform",
"popular-mangos-rest",
@ -436,6 +456,7 @@
"serious-poems-brake",
"serious-socks-cover",
"serious-zebras-scream",
"seven-bees-tell",
"seven-deers-jam",
"seven-garlics-serve",
"seven-hornets-smile",
@ -467,6 +488,7 @@
"silver-sheep-knock",
"six-bears-trade",
"six-boats-shave",
"six-gorillas-obey",
"sixty-items-crash",
"sixty-numbers-hope",
"sixty-pandas-rush",
@ -519,6 +541,7 @@
"stale-comics-look",
"stale-fans-rest",
"stale-jeans-refuse",
"stale-nails-listen",
"strange-apricots-happen",
"strange-roses-brake",
"strong-apricots-destroy",
@ -526,6 +549,7 @@
"strong-lemons-provide",
"strong-pans-doubt",
"stupid-parents-crash",
"sweet-bottles-check",
"sweet-mangos-beg",
"sweet-pens-sniff",
"swift-donkeys-perform",
@ -533,6 +557,7 @@
"swift-feet-juggle",
"swift-knives-tie",
"swift-poets-carry",
"swift-rats-sing",
"swift-ravens-hunt",
"swift-seahorses-deliver",
"tall-books-grin",
@ -549,12 +574,14 @@
"tasty-steaks-smile",
"ten-eels-move",
"ten-foxes-repeat",
"ten-geese-share",
"ten-jokes-divide",
"ten-peaches-sleep",
"ten-singers-cough",
"ten-teachers-travel",
"ten-ties-repair",
"ten-worms-reflect",
"tender-lemons-judge",
"tender-rocks-walk",
"thick-cycles-rule",
"thick-pans-tell",
@ -588,11 +615,13 @@
"tiny-taxis-whisper",
"tough-radios-punch",
"tough-tomatoes-explain",
"tricky-laws-bathe",
"twelve-beans-drive",
"twelve-cows-learn",
"twelve-dragons-join",
"twelve-onions-juggle",
"twelve-worms-jog",
"twenty-gifts-develop",
"two-brooms-fail",
"two-candles-move",
"two-dogs-accept",
@ -608,6 +637,7 @@
"weak-drinks-speak",
"weak-frogs-bow",
"weak-terms-destroy",
"wet-bats-exercise",
"wet-games-fly",
"wet-pears-remain",
"wet-wombats-repeat",
@ -615,6 +645,7 @@
"wicked-doors-train",
"wicked-hairs-cheer",
"wicked-wasps-allow",
"wicked-ways-reply",
"wild-foxes-wonder",
"wild-moose-compare",
"wise-apples-care",

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: allow slot attribute inside snippets

@ -0,0 +1,5 @@
---
'svelte': patch
---
feat: allow `let props = $props()` and optimize prop read access

@ -0,0 +1,5 @@
---
"svelte": patch
---
chore: remove anchor node from each block items

@ -0,0 +1,5 @@
---
"svelte": patch
---
chore: improve runtime performance of capturing reactive signals

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: prevent `a11y_label_has_associated_control` false positive for component or render tag in `<label>`

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: allow multiple optional parameters with defaults in snippets

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: improve await block behaviour in non-runes mode

@ -0,0 +1,5 @@
---
"svelte": patch
---
feat: improve type arguments for Snippet and $bindable

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: remove document event listeners on unmount

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: preserve component function context for nested components

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: improve select handling of dynamic value with placeholders

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: deconflict multiple snippets of the same name

@ -60,3 +60,17 @@ jobs:
- name: build and check generated types
if: (${{ success() }} || ${{ failure() }}) # ensures this step runs even if previous steps fail
run: pnpm build && { [ "`git status --porcelain=v1`" == "" ] || (echo "Generated types have changed — please regenerate types locally and commit the changes after you have reviewed them"; git diff; exit 1); }
Benchmarks:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v3
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v3
with:
node-version: 18
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm bench
env:
CI: true

2
.gitignore vendored

@ -22,3 +22,5 @@ coverage
.DS_Store
tmp
benchmarking/compare/.results

@ -1,5 +1,6 @@
{
"search.exclude": {
"sites/svelte-5-preview/static/*": true
}
},
"typescript.tsdk": "node_modules/typescript/lib"
}

@ -0,0 +1,58 @@
import {
kairo_avoidable_owned,
kairo_avoidable_unowned
} from './benchmarks/kairo/kairo_avoidable.js';
import { kairo_broad_owned, kairo_broad_unowned } from './benchmarks/kairo/kairo_broad.js';
import { kairo_deep_owned, kairo_deep_unowned } from './benchmarks/kairo/kairo_deep.js';
import { kairo_diamond_owned, kairo_diamond_unowned } from './benchmarks/kairo/kairo_diamond.js';
import { kairo_mux_unowned, kairo_mux_owned } from './benchmarks/kairo/kairo_mux.js';
import { kairo_repeated_unowned, kairo_repeated_owned } from './benchmarks/kairo/kairo_repeated.js';
import { kairo_triangle_owned, kairo_triangle_unowned } from './benchmarks/kairo/kairo_triangle.js';
import { kairo_unstable_owned, kairo_unstable_unowned } from './benchmarks/kairo/kairo_unstable.js';
import { mol_bench_owned, mol_bench_unowned } from './benchmarks/mol_bench.js';
import {
sbench_create_0to1,
sbench_create_1000to1,
sbench_create_1to1,
sbench_create_1to1000,
sbench_create_1to2,
sbench_create_1to4,
sbench_create_1to8,
sbench_create_2to1,
sbench_create_4to1,
sbench_create_signals
} from './benchmarks/sbench.js';
// This benchmark has been adapted from the js-reactivity-benchmark (https://github.com/milomg/js-reactivity-benchmark)
// Not all tests are the same, and many parts have been tweaked to capture different data.
export const benchmarks = [
sbench_create_signals,
sbench_create_0to1,
sbench_create_1to1,
sbench_create_2to1,
sbench_create_4to1,
sbench_create_1000to1,
sbench_create_1to2,
sbench_create_1to4,
sbench_create_1to8,
sbench_create_1to1000,
kairo_avoidable_owned,
kairo_avoidable_unowned,
kairo_broad_owned,
kairo_broad_unowned,
kairo_deep_owned,
kairo_deep_unowned,
kairo_diamond_owned,
kairo_diamond_unowned,
kairo_triangle_owned,
kairo_triangle_unowned,
kairo_mux_owned,
kairo_mux_unowned,
kairo_repeated_owned,
kairo_repeated_unowned,
kairo_unstable_owned,
kairo_unstable_unowned,
mol_bench_owned,
mol_bench_unowned
];

@ -0,0 +1,91 @@
import { assert, fastest_test } from '../../utils.js';
import * as $ from '../../../packages/svelte/src/internal/client/index.js';
import { busy } from './util.js';
function setup() {
let head = $.source(0);
let computed1 = $.derived(() => $.get(head));
let computed2 = $.derived(() => ($.get(computed1), 0));
let computed3 = $.derived(() => (busy(), $.get(computed2) + 1)); // heavy computation
let computed4 = $.derived(() => $.get(computed3) + 2);
let computed5 = $.derived(() => $.get(computed4) + 3);
const destroy = $.effect_root(() => {
$.render_effect(() => {
$.get(computed5);
busy(); // heavy side effect
});
});
return {
destroy,
run() {
$.flush_sync(() => {
$.set(head, 1);
});
assert($.get(computed5) === 6);
for (let i = 0; i < 1000; i++) {
$.flush_sync(() => {
$.set(head, i);
});
assert($.get(computed5) === 6);
}
}
};
}
export async function kairo_avoidable_unowned() {
// Do 10 loops to warm up JIT
for (let i = 0; i < 10; i++) {
const { run, destroy } = setup();
run();
destroy();
}
const { run, destroy } = setup();
const { timing } = await fastest_test(10, () => {
for (let i = 0; i < 100; i++) {
run();
}
});
destroy();
return {
benchmark: 'kairo_avoidable_unowned',
time: timing.time.toFixed(2),
gc_time: timing.gc_time.toFixed(2)
};
}
export async function kairo_avoidable_owned() {
let run, destroy;
const destroy_owned = $.effect_root(() => {
// Do 10 loops to warm up JIT
for (let i = 0; i < 10; i++) {
const { run, destroy } = setup();
run();
destroy();
}
({ run, destroy } = setup());
});
const { timing } = await fastest_test(10, () => {
for (let i = 0; i < 100; i++) {
run();
}
});
// @ts-ignore
destroy();
destroy_owned();
return {
benchmark: 'kairo_avoidable_owned',
time: timing.time.toFixed(2),
gc_time: timing.gc_time.toFixed(2)
};
}

@ -0,0 +1,97 @@
import { assert, fastest_test } from '../../utils.js';
import * as $ from '../../../packages/svelte/src/internal/client/index.js';
function setup() {
let head = $.source(0);
let last = head;
let counter = 0;
const destroy = $.effect_root(() => {
for (let i = 0; i < 50; i++) {
let current = $.derived(() => {
return $.get(head) + i;
});
let current2 = $.derived(() => {
return $.get(current) + 1;
});
$.render_effect(() => {
$.get(current2);
counter++;
});
last = current2;
}
});
return {
destroy,
run() {
$.flush_sync(() => {
$.set(head, 1);
});
counter = 0;
for (let i = 0; i < 50; i++) {
$.flush_sync(() => {
$.set(head, i);
});
assert($.get(last) === i + 50);
}
assert(counter === 50 * 50);
}
};
}
export async function kairo_broad_unowned() {
// Do 10 loops to warm up JIT
for (let i = 0; i < 10; i++) {
const { run, destroy } = setup();
run();
destroy();
}
const { run, destroy } = setup();
const { timing } = await fastest_test(10, () => {
for (let i = 0; i < 100; i++) {
run();
}
});
destroy();
return {
benchmark: 'kairo_broad_unowned',
time: timing.time.toFixed(2),
gc_time: timing.gc_time.toFixed(2)
};
}
export async function kairo_broad_owned() {
let run, destroy;
const destroy_owned = $.effect_root(() => {
// Do 10 loops to warm up JIT
for (let i = 0; i < 10; i++) {
const { run, destroy } = setup();
run();
destroy();
}
({ run, destroy } = setup());
});
const { timing } = await fastest_test(10, () => {
for (let i = 0; i < 100; i++) {
run();
}
});
// @ts-ignore
destroy();
destroy_owned();
return {
benchmark: 'kairo_broad_owned',
time: timing.time.toFixed(2),
gc_time: timing.gc_time.toFixed(2)
};
}

@ -0,0 +1,97 @@
import { assert, fastest_test } from '../../utils.js';
import * as $ from '../../../packages/svelte/src/internal/client/index.js';
let len = 50;
const iter = 50;
function setup() {
let head = $.source(0);
let current = head;
for (let i = 0; i < len; i++) {
let c = current;
current = $.derived(() => {
return $.get(c) + 1;
});
}
let counter = 0;
const destroy = $.effect_root(() => {
$.render_effect(() => {
$.get(current);
counter++;
});
});
return {
destroy,
run() {
$.flush_sync(() => {
$.set(head, 1);
});
counter = 0;
for (let i = 0; i < iter; i++) {
$.flush_sync(() => {
$.set(head, i);
});
assert($.get(current) === len + i);
}
assert(counter === iter);
}
};
}
export async function kairo_deep_unowned() {
// Do 10 loops to warm up JIT
for (let i = 0; i < 10; i++) {
const { run, destroy } = setup();
run();
destroy();
}
const { run, destroy } = setup();
const { timing } = await fastest_test(10, () => {
for (let i = 0; i < 100; i++) {
run();
}
});
destroy();
return {
benchmark: 'kairo_deep_unowned',
time: timing.time.toFixed(2),
gc_time: timing.gc_time.toFixed(2)
};
}
export async function kairo_deep_owned() {
let run, destroy;
const destroy_owned = $.effect_root(() => {
// Do 10 loops to warm up JIT
for (let i = 0; i < 10; i++) {
const { run, destroy } = setup();
run();
destroy();
}
({ run, destroy } = setup());
});
const { timing } = await fastest_test(10, () => {
for (let i = 0; i < 100; i++) {
run();
}
});
// @ts-ignore
destroy();
destroy_owned();
return {
benchmark: 'kairo_deep_owned',
time: timing.time.toFixed(2),
gc_time: timing.gc_time.toFixed(2)
};
}

@ -0,0 +1,101 @@
import { assert, fastest_test } from '../../utils.js';
import * as $ from '../../../packages/svelte/src/internal/client/index.js';
let width = 5;
function setup() {
let head = $.source(0);
let current = [];
for (let i = 0; i < width; i++) {
current.push(
$.derived(() => {
return $.get(head) + 1;
})
);
}
let sum = $.derived(() => {
return current.map((x) => $.get(x)).reduce((a, b) => a + b, 0);
});
let counter = 0;
const destroy = $.effect_root(() => {
$.render_effect(() => {
$.get(sum);
counter++;
});
});
return {
destroy,
run() {
$.flush_sync(() => {
$.set(head, 1);
});
assert($.get(sum) === 2 * width);
counter = 0;
for (let i = 0; i < 500; i++) {
$.flush_sync(() => {
$.set(head, i);
});
assert($.get(sum) === (i + 1) * width);
}
assert(counter === 500);
}
};
}
export async function kairo_diamond_unowned() {
// Do 10 loops to warm up JIT
for (let i = 0; i < 10; i++) {
const { run, destroy } = setup();
run();
destroy();
}
const { run, destroy } = setup();
const { timing } = await fastest_test(10, () => {
for (let i = 0; i < 100; i++) {
run();
}
});
destroy();
return {
benchmark: 'kairo_diamond_unowned',
time: timing.time.toFixed(2),
gc_time: timing.gc_time.toFixed(2)
};
}
export async function kairo_diamond_owned() {
let run, destroy;
const destroy_owned = $.effect_root(() => {
// Do 10 loops to warm up JIT
for (let i = 0; i < 10; i++) {
const { run, destroy } = setup();
run();
destroy();
}
({ run, destroy } = setup());
});
const { timing } = await fastest_test(10, () => {
for (let i = 0; i < 100; i++) {
run();
}
});
// @ts-ignore
destroy();
destroy_owned();
return {
benchmark: 'kairo_diamond_owned',
time: timing.time.toFixed(2),
gc_time: timing.gc_time.toFixed(2)
};
}

@ -0,0 +1,94 @@
import { assert, fastest_test } from '../../utils.js';
import * as $ from '../../../packages/svelte/src/internal/client/index.js';
function setup() {
let heads = new Array(100).fill(null).map((_) => $.source(0));
const mux = $.derived(() => {
return Object.fromEntries(heads.map((h) => $.get(h)).entries());
});
const splited = heads
.map((_, index) => $.derived(() => $.get(mux)[index]))
.map((x) => $.derived(() => $.get(x) + 1));
const destroy = $.effect_root(() => {
splited.forEach((x) => {
$.render_effect(() => {
$.get(x);
});
});
});
return {
destroy,
run() {
for (let i = 0; i < 10; i++) {
$.flush_sync(() => {
$.set(heads[i], i);
});
assert($.get(splited[i]) === i + 1);
}
for (let i = 0; i < 10; i++) {
$.flush_sync(() => {
$.set(heads[i], i * 2);
});
assert($.get(splited[i]) === i * 2 + 1);
}
}
};
}
export async function kairo_mux_unowned() {
// Do 10 loops to warm up JIT
for (let i = 0; i < 10; i++) {
const { run, destroy } = setup();
run();
destroy();
}
const { run, destroy } = setup();
const { timing } = await fastest_test(10, () => {
for (let i = 0; i < 100; i++) {
run();
}
});
destroy();
return {
benchmark: 'kairo_mux_unowned',
time: timing.time.toFixed(2),
gc_time: timing.gc_time.toFixed(2)
};
}
export async function kairo_mux_owned() {
let run, destroy;
const destroy_owned = $.effect_root(() => {
// Do 10 loops to warm up JIT
for (let i = 0; i < 10; i++) {
const { run, destroy } = setup();
run();
destroy();
}
({ run, destroy } = setup());
});
const { timing } = await fastest_test(10, () => {
for (let i = 0; i < 100; i++) {
run();
}
});
// @ts-ignore
destroy();
destroy_owned();
return {
benchmark: 'kairo_mux_owned',
time: timing.time.toFixed(2),
gc_time: timing.gc_time.toFixed(2)
};
}

@ -0,0 +1,98 @@
import { assert, fastest_test } from '../../utils.js';
import * as $ from '../../../packages/svelte/src/internal/client/index.js';
let size = 30;
function setup() {
let head = $.source(0);
let current = $.derived(() => {
let result = 0;
for (let i = 0; i < size; i++) {
result += $.get(head);
}
return result;
});
let counter = 0;
const destroy = $.effect_root(() => {
$.render_effect(() => {
$.get(current);
counter++;
});
});
return {
destroy,
run() {
$.flush_sync(() => {
$.set(head, 1);
});
assert($.get(current) === size);
counter = 0;
for (let i = 0; i < 100; i++) {
$.flush_sync(() => {
$.set(head, i);
});
assert($.get(current) === i * size);
}
assert(counter === 100);
}
};
}
export async function kairo_repeated_unowned() {
// Do 10 loops to warm up JIT
for (let i = 0; i < 10; i++) {
const { run, destroy } = setup();
run();
destroy();
}
const { run, destroy } = setup();
const { timing } = await fastest_test(10, () => {
for (let i = 0; i < 100; i++) {
run();
}
});
destroy();
return {
benchmark: 'kairo_repeated_unowned',
time: timing.time.toFixed(2),
gc_time: timing.gc_time.toFixed(2)
};
}
export async function kairo_repeated_owned() {
let run, destroy;
const destroy_owned = $.effect_root(() => {
// Do 10 loops to warm up JIT
for (let i = 0; i < 10; i++) {
const { run, destroy } = setup();
run();
destroy();
}
({ run, destroy } = setup());
});
const { timing } = await fastest_test(10, () => {
for (let i = 0; i < 100; i++) {
run();
}
});
// @ts-ignore
destroy();
destroy_owned();
return {
benchmark: 'kairo_repeated_owned',
time: timing.time.toFixed(2),
gc_time: timing.gc_time.toFixed(2)
};
}

@ -0,0 +1,111 @@
import { assert, fastest_test } from '../../utils.js';
import * as $ from '../../../packages/svelte/src/internal/client/index.js';
let width = 10;
function count(number) {
return new Array(number)
.fill(0)
.map((_, i) => i + 1)
.reduce((x, y) => x + y, 0);
}
function setup() {
let head = $.source(0);
let current = head;
let list = [];
for (let i = 0; i < width; i++) {
let c = current;
list.push(current);
current = $.derived(() => {
return $.get(c) + 1;
});
}
let sum = $.derived(() => {
return list.map((x) => $.get(x)).reduce((a, b) => a + b, 0);
});
let counter = 0;
const destroy = $.effect_root(() => {
$.render_effect(() => {
$.get(sum);
counter++;
});
});
return {
destroy,
run() {
const constant = count(width);
$.flush_sync(() => {
$.set(head, 1);
});
assert($.get(sum) === constant);
counter = 0;
for (let i = 0; i < 100; i++) {
$.flush_sync(() => {
$.set(head, i);
});
assert($.get(sum) === constant - width + i * width);
}
assert(counter === 100);
}
};
}
export async function kairo_triangle_unowned() {
// Do 10 loops to warm up JIT
for (let i = 0; i < 10; i++) {
const { run, destroy } = setup();
run();
destroy();
}
const { run, destroy } = setup();
const { timing } = await fastest_test(10, () => {
for (let i = 0; i < 100; i++) {
run();
}
});
destroy();
return {
benchmark: 'kairo_triangle_unowned',
time: timing.time.toFixed(2),
gc_time: timing.gc_time.toFixed(2)
};
}
export async function kairo_triangle_owned() {
let run, destroy;
const destroy_owned = $.effect_root(() => {
// Do 10 loops to warm up JIT
for (let i = 0; i < 10; i++) {
const { run, destroy } = setup();
run();
destroy();
}
({ run, destroy } = setup());
});
const { timing } = await fastest_test(10, () => {
for (let i = 0; i < 100; i++) {
run();
}
});
// @ts-ignore
destroy();
destroy_owned();
return {
benchmark: 'kairo_triangle_owned',
time: timing.time.toFixed(2),
gc_time: timing.gc_time.toFixed(2)
};
}

@ -0,0 +1,97 @@
import { assert, fastest_test } from '../../utils.js';
import * as $ from '../../../packages/svelte/src/internal/client/index.js';
function setup() {
let head = $.source(0);
const double = $.derived(() => $.get(head) * 2);
const inverse = $.derived(() => -$.get(head));
let current = $.derived(() => {
let result = 0;
for (let i = 0; i < 20; i++) {
result += $.get(head) % 2 ? $.get(double) : $.get(inverse);
}
return result;
});
let counter = 0;
const destroy = $.effect_root(() => {
$.render_effect(() => {
$.get(current);
counter++;
});
});
return {
destroy,
run() {
$.flush_sync(() => {
$.set(head, 1);
});
assert($.get(current) === 40);
counter = 0;
for (let i = 0; i < 100; i++) {
$.flush_sync(() => {
$.set(head, i);
});
}
assert(counter === 100);
}
};
}
export async function kairo_unstable_unowned() {
// Do 10 loops to warm up JIT
for (let i = 0; i < 10; i++) {
const { run, destroy } = setup();
run();
destroy();
}
const { run, destroy } = setup();
const { timing } = await fastest_test(10, () => {
for (let i = 0; i < 100; i++) {
run();
}
});
destroy();
return {
benchmark: 'kairo_unstable_unowned',
time: timing.time.toFixed(2),
gc_time: timing.gc_time.toFixed(2)
};
}
export async function kairo_unstable_owned() {
let run, destroy;
const destroy_owned = $.effect_root(() => {
// Do 10 loops to warm up JIT
for (let i = 0; i < 10; i++) {
const { run, destroy } = setup();
run();
destroy();
}
({ run, destroy } = setup());
});
const { timing } = await fastest_test(10, () => {
for (let i = 0; i < 100; i++) {
run();
}
});
// @ts-ignore
destroy();
destroy_owned();
return {
benchmark: 'kairo_unstable_owned',
time: timing.time.toFixed(2),
gc_time: timing.gc_time.toFixed(2)
};
}

@ -0,0 +1,7 @@
export function busy() {
let a = 0;
for (let i = 0; i < 1_00; i++) {
a++;
}
}

@ -0,0 +1,121 @@
import { assert, fastest_test } from '../utils.js';
import * as $ from '../../packages/svelte/src/internal/client/index.js';
/**
* @param {number} n
*/
function fib(n) {
if (n < 2) return 1;
return fib(n - 1) + fib(n - 2);
}
/**
* @param {number} n
*/
function hard(n) {
return n + fib(16);
}
const numbers = Array.from({ length: 5 }, (_, i) => i);
function setup() {
let res = [];
const A = $.source(0);
const B = $.source(0);
const C = $.derived(() => ($.get(A) % 2) + ($.get(B) % 2));
const D = $.derived(() => numbers.map((i) => i + ($.get(A) % 2) - ($.get(B) % 2)));
D.equals = function (/** @type {number[]} */ l) {
var r = this.v;
return r !== null && l.length === r.length && l.every((v, i) => v === r[i]);
};
const E = $.derived(() => hard($.get(C) + $.get(A) + $.get(D)[0]));
const F = $.derived(() => hard($.get(D)[0] && $.get(B)));
const G = $.derived(() => $.get(C) + ($.get(C) || $.get(E) % 2) + $.get(D)[0] + $.get(F));
const destroy = $.effect_root(() => {
$.render_effect(() => {
res.push(hard($.get(G)));
});
$.render_effect(() => {
res.push($.get(G));
});
$.render_effect(() => {
res.push(hard($.get(F)));
});
});
return {
destroy,
/**
* @param {number} i
*/
run(i) {
res.length = 0;
$.flush_sync(() => {
$.set(B, 1);
$.set(A, 1 + i * 2);
});
$.flush_sync(() => {
$.set(A, 2 + i * 2);
$.set(B, 2);
});
assert(res[0] === 3198 && res[1] === 1601 && res[2] === 3195 && res[3] === 1598);
}
};
}
export async function mol_bench_owned() {
let run, destroy;
const destroy_owned = $.effect_root(() => {
// Do 10 loops to warm up JIT
for (let i = 0; i < 10; i++) {
const { run, destroy } = setup();
run(0);
destroy();
}
({ run, destroy } = setup());
});
const { timing } = await fastest_test(10, () => {
for (let i = 0; i < 1e4; i++) {
run(i);
}
});
// @ts-ignore
destroy();
destroy_owned();
return {
benchmark: 'mol_bench_owned',
time: timing.time.toFixed(2),
gc_time: timing.gc_time.toFixed(2)
};
}
export async function mol_bench_unowned() {
// Do 10 loops to warm up JIT
for (let i = 0; i < 10; i++) {
const { run, destroy } = setup();
run(0);
destroy();
}
const { run, destroy } = setup();
const { timing } = await fastest_test(10, () => {
for (let i = 0; i < 1e4; i++) {
run(i);
}
});
destroy();
return {
benchmark: 'mol_bench_unowned',
time: timing.time.toFixed(2),
gc_time: timing.gc_time.toFixed(2)
};
}

@ -0,0 +1,339 @@
import { fastest_test } from '../utils.js';
import * as $ from '../../packages/svelte/src/internal/client/index.js';
const COUNT = 1e5;
/**
* @param {number} n
* @param {any[]} sources
*/
function create_data_signals(n, sources) {
for (let i = 0; i < n; i++) {
sources[i] = $.source(i);
}
return sources;
}
/**
* @param {number} i
*/
function create_computation_0(i) {
$.derived(() => i);
}
/**
* @param {any} s1
*/
function create_computation_1(s1) {
$.derived(() => $.get(s1));
}
/**
* @param {any} s1
* @param {any} s2
*/
function create_computation_2(s1, s2) {
$.derived(() => $.get(s1) + $.get(s2));
}
function create_computation_1000(ss, offset) {
$.derived(() => {
let sum = 0;
for (let i = 0; i < 1000; i++) {
sum += $.get(ss[offset + i]);
}
return sum;
});
}
/**
* @param {number} n
*/
function create_computations_0to1(n) {
for (let i = 0; i < n; i++) {
create_computation_0(i);
}
}
/**
* @param {number} n
* @param {any[]} sources
*/
function create_computations_1to1(n, sources) {
for (let i = 0; i < n; i++) {
const source = sources[i];
create_computation_1(source);
}
}
/**
* @param {number} n
* @param {any[]} sources
*/
function create_computations_2to1(n, sources) {
for (let i = 0; i < n; i++) {
create_computation_2(sources[i * 2], sources[i * 2 + 1]);
}
}
function create_computation_4(s1, s2, s3, s4) {
$.derived(() => $.get(s1) + $.get(s2) + $.get(s3) + $.get(s4));
}
function create_computations_1000to1(n, sources) {
for (let i = 0; i < n; i++) {
create_computation_1000(sources, i * 1000);
}
}
function create_computations_1to2(n, sources) {
for (let i = 0; i < n / 2; i++) {
const source = sources[i];
create_computation_1(source);
create_computation_1(source);
}
}
function create_computations_1to4(n, sources) {
for (let i = 0; i < n / 4; i++) {
const source = sources[i];
create_computation_1(source);
create_computation_1(source);
create_computation_1(source);
create_computation_1(source);
}
}
function create_computations_1to8(n, sources) {
for (let i = 0; i < n / 8; i++) {
const source = sources[i];
create_computation_1(source);
create_computation_1(source);
create_computation_1(source);
create_computation_1(source);
create_computation_1(source);
create_computation_1(source);
create_computation_1(source);
create_computation_1(source);
}
}
function create_computations_1to1000(n, sources) {
for (let i = 0; i < n / 1000; i++) {
const source = sources[i];
for (let j = 0; j < 1000; j++) {
create_computation_1(source);
}
}
}
function create_computations_4to1(n, sources) {
for (let i = 0; i < n; i++) {
create_computation_4(
sources[i * 4],
sources[i * 4 + 1],
sources[i * 4 + 2],
sources[i * 4 + 3]
);
}
}
/**
* @param {any} fn
* @param {number} count
* @param {number} scount
*/
function bench(fn, count, scount) {
let sources = create_data_signals(scount, []);
fn(count, sources);
}
export async function sbench_create_signals() {
// Do 3 loops to warm up JIT
for (let i = 0; i < 3; i++) {
bench(create_data_signals, COUNT, COUNT);
}
const { timing } = await fastest_test(10, () => {
for (let i = 0; i < 100; i++) {
bench(create_data_signals, COUNT, COUNT);
}
});
return {
benchmark: 'sbench_create_signals',
time: timing.time.toFixed(2),
gc_time: timing.gc_time.toFixed(2)
};
}
export async function sbench_create_0to1() {
// Do 3 loops to warm up JIT
for (let i = 0; i < 3; i++) {
bench(create_computations_0to1, COUNT, 0);
}
const { timing } = await fastest_test(10, () => {
for (let i = 0; i < 100; i++) {
bench(create_computations_0to1, COUNT, 0);
}
});
return {
benchmark: 'sbench_create_0to1',
time: timing.time.toFixed(2),
gc_time: timing.gc_time.toFixed(2)
};
}
export async function sbench_create_1to1() {
// Do 3 loops to warm up JIT
for (let i = 0; i < 3; i++) {
bench(create_computations_1to1, COUNT, COUNT);
}
const { timing } = await fastest_test(10, () => {
for (let i = 0; i < 100; i++) {
bench(create_computations_1to1, COUNT, COUNT);
}
});
return {
benchmark: 'sbench_create_1to1',
time: timing.time.toFixed(2),
gc_time: timing.gc_time.toFixed(2)
};
}
export async function sbench_create_2to1() {
// Do 3 loops to warm up JIT
for (let i = 0; i < 3; i++) {
bench(create_computations_2to1, COUNT / 2, COUNT);
}
const { timing } = await fastest_test(10, () => {
for (let i = 0; i < 100; i++) {
bench(create_computations_2to1, COUNT / 2, COUNT);
}
});
return {
benchmark: 'sbench_create_2to1',
time: timing.time.toFixed(2),
gc_time: timing.gc_time.toFixed(2)
};
}
export async function sbench_create_4to1() {
// Do 3 loops to warm up JIT
for (let i = 0; i < 3; i++) {
bench(create_computations_4to1, COUNT / 4, COUNT);
}
const { timing } = await fastest_test(10, () => {
for (let i = 0; i < 100; i++) {
bench(create_computations_4to1, COUNT / 4, COUNT);
}
});
return {
benchmark: 'sbench_create_4to1',
time: timing.time.toFixed(2),
gc_time: timing.gc_time.toFixed(2)
};
}
export async function sbench_create_1000to1() {
// Do 3 loops to warm up JIT
for (let i = 0; i < 3; i++) {
bench(create_computations_1000to1, COUNT / 1000, COUNT);
}
const { timing } = await fastest_test(10, () => {
for (let i = 0; i < 100; i++) {
bench(create_computations_1000to1, COUNT / 1000, COUNT);
}
});
return {
benchmark: 'sbench_create_1000to1',
time: timing.time.toFixed(2),
gc_time: timing.gc_time.toFixed(2)
};
}
export async function sbench_create_1to2() {
// Do 3 loops to warm up JIT
for (let i = 0; i < 3; i++) {
bench(create_computations_1to2, COUNT, COUNT / 2);
}
const { timing } = await fastest_test(10, () => {
for (let i = 0; i < 100; i++) {
bench(create_computations_1to2, COUNT, COUNT / 2);
}
});
return {
benchmark: 'sbench_create_1to2',
time: timing.time.toFixed(2),
gc_time: timing.gc_time.toFixed(2)
};
}
export async function sbench_create_1to4() {
// Do 3 loops to warm up JIT
for (let i = 0; i < 3; i++) {
bench(create_computations_1to4, COUNT, COUNT / 4);
}
const { timing } = await fastest_test(10, () => {
for (let i = 0; i < 100; i++) {
bench(create_computations_1to4, COUNT, COUNT / 4);
}
});
return {
benchmark: 'sbench_create_1to4',
time: timing.time.toFixed(2),
gc_time: timing.gc_time.toFixed(2)
};
}
export async function sbench_create_1to8() {
// Do 3 loops to warm up JIT
for (let i = 0; i < 3; i++) {
bench(create_computations_1to8, COUNT, COUNT / 8);
}
const { timing } = await fastest_test(10, () => {
for (let i = 0; i < 100; i++) {
bench(create_computations_1to8, COUNT, COUNT / 8);
}
});
return {
benchmark: 'sbench_create_1to8',
time: timing.time.toFixed(2),
gc_time: timing.gc_time.toFixed(2)
};
}
export async function sbench_create_1to1000() {
// Do 3 loops to warm up JIT
for (let i = 0; i < 3; i++) {
bench(create_computations_1to1000, COUNT, COUNT / 1000);
}
const { timing } = await fastest_test(10, () => {
for (let i = 0; i < 100; i++) {
bench(create_computations_1to1000, COUNT, COUNT / 1000);
}
});
return {
benchmark: 'sbench_create_1to1000',
time: timing.time.toFixed(2),
gc_time: timing.gc_time.toFixed(2)
};
}

@ -0,0 +1,90 @@
import fs from 'node:fs';
import path from 'node:path';
import { execSync, fork } from 'node:child_process';
import { fileURLToPath } from 'node:url';
import { benchmarks } from '../benchmarks.js';
// if (execSync('git status --porcelain').toString().trim()) {
// console.error('Working directory is not clean');
// process.exit(1);
// }
const filename = fileURLToPath(import.meta.url);
const runner = path.resolve(filename, '../runner.js');
const outdir = path.resolve(filename, '../.results');
if (fs.existsSync(outdir)) fs.rmSync(outdir, { recursive: true });
fs.mkdirSync(outdir);
const branches = [];
for (const arg of process.argv.slice(2)) {
if (arg.startsWith('--')) continue;
if (arg === filename) continue;
branches.push(arg);
}
if (branches.length === 0) {
branches.push(
execSync('git symbolic-ref --short -q HEAD || git rev-parse --short HEAD').toString().trim()
);
}
if (branches.length === 1) {
branches.push('main');
}
process.on('exit', () => {
execSync(`git checkout ${branches[0]}`);
});
for (const branch of branches) {
console.group(`Benchmarking ${branch}`);
execSync(`git checkout ${branch}`);
await new Promise((fulfil, reject) => {
const child = fork(runner);
child.on('message', (results) => {
fs.writeFileSync(`${outdir}/${branch}.json`, JSON.stringify(results, null, ' '));
fulfil();
});
child.on('error', reject);
});
console.groupEnd();
}
const results = branches.map((branch) => {
return JSON.parse(fs.readFileSync(`${outdir}/${branch}.json`, 'utf-8'));
});
for (let i = 0; i < results[0].length; i += 1) {
console.group(`${results[0][i].benchmark}`);
for (const metric of ['time', 'gc_time']) {
const times = results.map((result) => +result[i][metric]);
let min = Infinity;
let min_index = -1;
for (let b = 0; b < times.length; b += 1) {
if (times[b] < min) {
min = times[b];
min_index = b;
}
}
if (min !== 0) {
console.group(`${metric}: fastest is ${branches[min_index]}`);
times.forEach((time, b) => {
console.log(`${branches[b]}: ${time.toFixed(2)}ms (${((time / min) * 100).toFixed(2)}%)`);
});
console.groupEnd();
}
}
console.groupEnd();
}

@ -0,0 +1,10 @@
import { benchmarks } from '../benchmarks.js';
const results = [];
for (const benchmark of benchmarks) {
const result = await benchmark();
console.error(result.benchmark);
results.push(result);
}
process.send(results);

@ -0,0 +1,32 @@
import * as $ from '../packages/svelte/src/internal/client/index.js';
import { benchmarks } from './benchmarks.js';
let total_time = 0;
let total_gc_time = 0;
// eslint-disable-next-line no-console
console.log('-- Benchmarking Started --');
$.push({}, true);
try {
for (const benchmark of benchmarks) {
const results = await benchmark();
// eslint-disable-next-line no-console
console.log(results);
total_time += Number(results.time);
total_gc_time += Number(results.gc_time);
}
} catch (e) {
// eslint-disable-next-line no-console
console.error('-- Benchmarking Failed --');
// eslint-disable-next-line no-console
console.error(e);
process.exit(1);
}
$.pop();
// eslint-disable-next-line no-console
console.log('-- Benchmarking Complete --');
// eslint-disable-next-line no-console
console.log({
total_time: total_time.toFixed(2),
total_gc_time: total_gc_time.toFixed(2)
});

@ -0,0 +1,17 @@
{
"compilerOptions": {
"moduleResolution": "Bundler",
"target": "ESNext",
"module": "ESNext",
"verbatimModuleSyntax": true,
"isolatedModules": true,
"resolveJsonModule": true,
"sourceMap": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"allowJs": true,
"checkJs": true
},
"include": ["./run.js", "./utils.js", "./benchmarks"]
}

@ -0,0 +1,98 @@
import { performance, PerformanceObserver } from 'node:perf_hooks';
import v8 from 'v8-natives';
// Credit to https://github.com/milomg/js-reactivity-benchmark for the logic for timing + GC tracking.
class GarbageTrack {
track_id = 0;
observer = new PerformanceObserver((list) => this.perf_entries.push(...list.getEntries()));
perf_entries = [];
periods = [];
watch(fn) {
this.track_id++;
const start = performance.now();
const result = fn();
const end = performance.now();
this.periods.push({ track_id: this.track_id, start, end });
return { result, track_id: this.track_id };
}
/**
* @param {number} track_id
*/
async gcDuration(track_id) {
await promise_delay(10);
const period = this.periods.find((period) => period.track_id === track_id);
if (!period) {
// eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors
return Promise.reject('no period found');
}
const entries = this.perf_entries.filter(
(e) => e.startTime >= period.start && e.startTime < period.end
);
return entries.reduce((t, e) => e.duration + t, 0);
}
destroy() {
this.observer.disconnect();
}
constructor() {
this.observer.observe({ entryTypes: ['gc'] });
}
}
function promise_delay(timeout = 0) {
return new Promise((resolve) => setTimeout(resolve, timeout));
}
/**
* @param {{ (): void; (): any; }} fn
*/
function run_timed(fn) {
const start = performance.now();
const result = fn();
const time = performance.now() - start;
return { result, time };
}
/**
* @param {() => void} fn
*/
async function run_tracked(fn) {
v8.collectGarbage();
const gc_track = new GarbageTrack();
const { result: wrappedResult, track_id } = gc_track.watch(() => run_timed(fn));
const gc_time = await gc_track.gcDuration(track_id);
const { result, time } = wrappedResult;
gc_track.destroy();
return { result, timing: { time, gc_time } };
}
/**
* @param {number} times
* @param {() => void} fn
*/
export async function fastest_test(times, fn) {
const results = [];
for (let i = 0; i < times; i++) {
const run = await run_tracked(fn);
results.push(run);
}
const fastest = results.reduce((a, b) => (a.timing.time < b.timing.time ? a : b));
return fastest;
}
/**
* @param {boolean} a
*/
export function assert(a) {
if (!a) {
throw new Error('Assertion failed');
}
}

@ -5,7 +5,7 @@
"private": true,
"type": "module",
"license": "MIT",
"packageManager": "pnpm@9.2.0",
"packageManager": "pnpm@9.4.0",
"engines": {
"pnpm": "^9.0.0"
},
@ -23,29 +23,31 @@
"test": "vitest run",
"test-output": "vitest run --coverage --reporter=json --outputFile=sites/svelte-5-preview/src/routes/status/results.json",
"changeset:version": "changeset version && pnpm -r generate:version && git add --all",
"changeset:publish": "changeset publish"
"changeset:publish": "changeset publish",
"bench": "node --allow-natives-syntax ./benchmarking/run.js",
"bench:compare": "node --allow-natives-syntax ./benchmarking/compare/index.js",
"bench:debug": "node --allow-natives-syntax --inspect-brk ./benchmarking/run.js"
},
"devDependencies": {
"@changesets/cli": "^2.27.1",
"@sveltejs/eslint-config": "^7.0.1",
"@changesets/cli": "^2.27.6",
"@sveltejs/eslint-config": "^8.0.1",
"@svitejs/changesets-changelog-github-compact": "^1.1.0",
"@types/node": "^20.11.5",
"@vitest/coverage-v8": "^1.2.1",
"concurrently": "^8.2.2",
"cross-env": "^7.0.3",
"eslint": "^9.0.0",
"eslint": "^9.6.0",
"eslint-plugin-lube": "^0.4.3",
"jsdom": "22.0.0",
"playwright": "^1.41.1",
"prettier": "^3.2.4",
"prettier-plugin-svelte": "^3.1.2",
"typescript": "^5.3.3",
"typescript-eslint": "^8.0.0-alpha.20",
"typescript": "^5.5.2",
"typescript-eslint": "^8.0.0-alpha.34",
"v8-natives": "^1.2.5",
"vitest": "^1.2.1"
},
"pnpm": {
"overrides": {
"jimp>xml2js": "^0.6.0"
"xml2js": "^0.6.0"
}
}
}

@ -1,5 +1,103 @@
# svelte
## 5.0.0-next.167
### Patch Changes
- fix: make more types from `svelte/compiler` public ([#12189](https://github.com/sveltejs/svelte/pull/12189))
- fix: support contenteditable binding undefined fallback ([#12210](https://github.com/sveltejs/svelte/pull/12210))
- breaking: prevent usage of arguments keyword in certain places ([#12191](https://github.com/sveltejs/svelte/pull/12191))
- fix(types): export CompileResult and Warning ([#12212](https://github.com/sveltejs/svelte/pull/12212))
- fix: ensure element dir properties persist with text changes ([#12204](https://github.com/sveltejs/svelte/pull/12204))
- fix: disallow accessing internal Svelte props ([#12207](https://github.com/sveltejs/svelte/pull/12207))
- fix: make media bindings more robust ([#12206](https://github.com/sveltejs/svelte/pull/12206))
- fix: allow slot attribute inside snippets ([#12188](https://github.com/sveltejs/svelte/pull/12188))
- feat: allow `let props = $props()` and optimize prop read access ([#12201](https://github.com/sveltejs/svelte/pull/12201))
- feat: improve type arguments for Snippet and $bindable ([#12197](https://github.com/sveltejs/svelte/pull/12197))
## 5.0.0-next.166
### Patch Changes
- fix: remove correct event listener from document ([#12101](https://github.com/sveltejs/svelte/pull/12101))
- fix: correctly serialize object assignment expressions ([#12175](https://github.com/sveltejs/svelte/pull/12175))
- fix: robustify migration script around indentation and comments ([#12176](https://github.com/sveltejs/svelte/pull/12176))
- fix: improve await block behaviour in non-runes mode ([#12179](https://github.com/sveltejs/svelte/pull/12179))
- fix: improve select handling of dynamic value with placeholders ([#12181](https://github.com/sveltejs/svelte/pull/12181))
## 5.0.0-next.165
### Patch Changes
- breaking: bump dts-buddy ([#12134](https://github.com/sveltejs/svelte/pull/12134))
- fix: throw compilation error for malformed snippets ([#12144](https://github.com/sveltejs/svelte/pull/12144))
## 5.0.0-next.164
### Patch Changes
- fix: prevent `a11y_label_has_associated_control` false positive for component or render tag in `<label>` ([#12119](https://github.com/sveltejs/svelte/pull/12119))
- fix: allow multiple optional parameters with defaults in snippets ([#12070](https://github.com/sveltejs/svelte/pull/12070))
## 5.0.0-next.163
### Patch Changes
- feat: more accurate `render`/`mount`/`hydrate` options ([#12111](https://github.com/sveltejs/svelte/pull/12111))
- fix: better binding interop between runes/non-runes components ([#12123](https://github.com/sveltejs/svelte/pull/12123))
## 5.0.0-next.162
### Patch Changes
- chore: remove anchor node from each block items ([#11836](https://github.com/sveltejs/svelte/pull/11836))
## 5.0.0-next.161
### Patch Changes
- fix: wait a microtask for await blocks to reduce UI churn ([#11989](https://github.com/sveltejs/svelte/pull/11989))
- fix: ensure state update expressions are serialised correctly ([#12109](https://github.com/sveltejs/svelte/pull/12109))
- fix: repair each block length even without an else ([#12098](https://github.com/sveltejs/svelte/pull/12098))
- fix: remove document event listeners on unmount ([#12105](https://github.com/sveltejs/svelte/pull/12105))
## 5.0.0-next.160
### Patch Changes
- chore: improve runtime performance of capturing reactive signals ([#12093](https://github.com/sveltejs/svelte/pull/12093))
## 5.0.0-next.159
### Patch Changes
- fix: ensure element size bindings don't unsubscribe multiple times from the resize observer ([#12091](https://github.com/sveltejs/svelte/pull/12091))
- fix: prevent misidentification of bindings as delegatable event handlers if used outside event attribute ([#12081](https://github.com/sveltejs/svelte/pull/12081))
- fix: preserve current input values when removing defaults ([#12083](https://github.com/sveltejs/svelte/pull/12083))
- fix: preserve component function context for nested components ([#12089](https://github.com/sveltejs/svelte/pull/12089))
## 5.0.0-next.158
### Patch Changes

@ -113,6 +113,7 @@ export interface DOMAttributes<T extends EventTarget> {
'on:beforeinput'?: EventHandler<InputEvent, T> | undefined | null;
onbeforeinput?: EventHandler<InputEvent, T> | undefined | null;
onbeforeinputcapture?: EventHandler<InputEvent, T> | undefined | null;
// oninput can be either an InputEvent or an Event, depending on the target element (input, textarea etc).
'on:input'?: FormEventHandler<T> | undefined | null;
oninput?: FormEventHandler<T> | undefined | null;
oninputcapture?: FormEventHandler<T> | undefined | null;

@ -50,6 +50,10 @@
> Imports of `svelte/internal/*` are forbidden. It contains private runtime code which is subject to change without notice. If you're importing from `svelte/internal/*` to work around a limitation of Svelte, please open an issue at https://github.com/sveltejs/svelte and explain your use case
## invalid_arguments_usage
> The arguments keyword cannot be used within the template or at the top level of a component
## legacy_export_invalid
> Cannot use `export let` in runes mode — use `$props()` instead
@ -74,6 +78,10 @@
> Cannot use `$props()` more than once
## props_illegal_name
> Declaring or accessing a prop starting with `$$` is illegal (they are reserved for Svelte internals)
## props_invalid_identifier
> `$props()` can only be used with an object destructuring pattern

@ -2,7 +2,7 @@
"name": "svelte",
"description": "Cybernetically enhanced web apps",
"license": "MIT",
"version": "5.0.0-next.158",
"version": "5.0.0-next.167",
"type": "module",
"types": "./types/index.d.ts",
"engines": {
@ -112,7 +112,7 @@
"prepublishOnly": "pnpm build",
"format": "prettier --check --write .",
"lint": "prettier --check . && eslint",
"knip": "knip"
"knip": "pnpm dlx knip"
},
"devDependencies": {
"@jridgewell/trace-mapping": "^0.3.22",
@ -122,9 +122,8 @@
"@rollup/plugin-terser": "^0.4.4",
"@rollup/plugin-virtual": "^3.0.2",
"@types/aria-query": "^5.0.4",
"dts-buddy": "^0.4.7",
"dts-buddy": "^0.5.0",
"esbuild": "^0.19.11",
"knip": "^4.2.1",
"rollup": "^4.9.5",
"source-map": "^0.7.4",
"tiny-glob": "^0.2.9"

@ -25,12 +25,12 @@ await createBundle({
[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}/compiler`]: `${dir}/src/compiler/public.d.ts`,
[`${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-client.js`,
[`${pkg.name}/server`]: `${dir}/src/server/index.js`,
[`${pkg.name}/server`]: `${dir}/src/server/index.d.ts`,
[`${pkg.name}/store`]: `${dir}/src/store/public.d.ts`,
[`${pkg.name}/transition`]: `${dir}/src/transition/public.d.ts`,
[`${pkg.name}/events`]: `${dir}/src/events/index.js`,

@ -306,7 +306,7 @@ declare function $props(): any;
*
* https://svelte-5-preview.vercel.app/docs/runes#$bindable
*/
declare function $bindable<T>(t?: T): T;
declare function $bindable<T>(fallback?: T): T;
/**
* Inspects one or more values whenever they, or the properties they contain, change. Example:

@ -213,6 +213,15 @@ export function import_svelte_internal_forbidden(node) {
e(node, "import_svelte_internal_forbidden", "Imports of `svelte/internal/*` are forbidden. It contains private runtime code which is subject to change without notice. If you're importing from `svelte/internal/*` to work around a limitation of Svelte, please open an issue at https://github.com/sveltejs/svelte and explain your use case");
}
/**
* The arguments keyword cannot be used within the template or at the top level of a component
* @param {null | number | NodeLike} node
* @returns {never}
*/
export function invalid_arguments_usage(node) {
e(node, "invalid_arguments_usage", "The arguments keyword cannot be used within the template or at the top level of a component");
}
/**
* Cannot use `export let` in runes mode use `$props()` instead
* @param {null | number | NodeLike} node
@ -267,6 +276,15 @@ export function props_duplicate(node) {
e(node, "props_duplicate", "Cannot use `$props()` more than once");
}
/**
* Declaring or accessing a prop starting with `$$` is illegal (they are reserved for Svelte internals)
* @param {null | number | NodeLike} node
* @returns {never}
*/
export function props_illegal_name(node) {
e(node, "props_illegal_name", "Declaring or accessing a prop starting with `$$` is illegal (they are reserved for Svelte internals)");
}
/**
* `$props()` can only be used with an object destructuring pattern
* @param {null | number | NodeLike} node

@ -1,3 +1,6 @@
/** @import { Expression } from 'estree' */
/** @import { BaseNode, ConstTag, Root, SvelteNode, TemplateNode, Text } from '#compiler' */
/** @import * as Legacy from './types/legacy-nodes.js' */
import { walk } from 'zimmerframe';
import {
regex_ends_with_whitespaces,
@ -8,7 +11,7 @@ import { extract_svelte_ignore } from './utils/extract_svelte_ignore.js';
/**
* Some of the legacy Svelte AST nodes remove whitespace from the start and end of their children.
* @param {import('./types/template.js').TemplateNode[]} nodes
* @param {TemplateNode[]} nodes
*/
function remove_surrounding_whitespace_nodes(nodes) {
const first = nodes.at(0);
@ -33,16 +36,13 @@ function remove_surrounding_whitespace_nodes(nodes) {
/**
* Transform our nice modern AST into the monstrosity emitted by Svelte 4
* @param {string} source
* @param {import('#compiler').Root} ast
* @returns {import('./types/legacy-nodes.js').LegacyRoot}
* @param {Root} ast
* @returns {Legacy.LegacyRoot}
*/
export function convert(source, ast) {
const root =
/** @type {import('./types/template.js').SvelteNode | import('./types/legacy-nodes.js').LegacySvelteNode} */ (
ast
);
const root = /** @type {SvelteNode | Legacy.LegacySvelteNode} */ (ast);
return /** @type {import('./types/legacy-nodes.js').LegacyRoot} */ (
return /** @type {Legacy.LegacyRoot} */ (
walk(root, null, {
_(node, { next }) {
// @ts-ignore
@ -74,8 +74,8 @@ export function convert(source, ast) {
let end = null;
if (node.fragment.nodes.length > 0) {
const first = /** @type {import('#compiler').BaseNode} */ (node.fragment.nodes.at(0));
const last = /** @type {import('#compiler').BaseNode} */ (node.fragment.nodes.at(-1));
const first = /** @type {BaseNode} */ (node.fragment.nodes.at(0));
const last = /** @type {BaseNode} */ (node.fragment.nodes.at(-1));
start = first.start;
end = last.end;
@ -229,25 +229,20 @@ export function convert(source, ast) {
end: node.end,
name: node.name,
attributes: node.attributes.map(
(child) =>
/** @type {import('./types/legacy-nodes.js').LegacyAttributeLike} */ (visit(child))
(child) => /** @type {Legacy.LegacyAttributeLike} */ (visit(child))
),
children: node.fragment.nodes.map(
(child) =>
/** @type {import('./types/legacy-nodes.js').LegacyElementLike} */ (visit(child))
(child) => /** @type {Legacy.LegacyElementLike} */ (visit(child))
)
};
},
// @ts-ignore
ConstTag(node) {
if (
/** @type {import('./types/legacy-nodes.js').LegacyConstTag} */ (node).expression !==
undefined
) {
if (/** @type {Legacy.LegacyConstTag} */ (node).expression !== undefined) {
return node;
}
const modern_node = /** @type {import('#compiler').ConstTag} */ (node);
const modern_node = /** @type {ConstTag} */ (node);
const { id: left } = { ...modern_node.declaration.declarations[0] };
// @ts-ignore
delete left.typeAnnotation;
@ -274,8 +269,7 @@ export function convert(source, ast) {
end: node.end,
expression: node.expression,
children: node.fragment.nodes.map(
(child) =>
/** @type {import('./types/legacy-nodes.js').LegacyElementLike} */ (visit(child))
(child) => /** @type {Legacy.LegacyElementLike} */ (visit(child))
)
};
},
@ -354,8 +348,7 @@ export function convert(source, ast) {
start,
end: end,
children: node.alternate.nodes.map(
(child) =>
/** @type {import('./types/legacy-nodes.js').LegacyElementLike} */ (visit(child))
(child) => /** @type {Legacy.LegacyElementLike} */ (visit(child))
)
};
}
@ -368,8 +361,7 @@ export function convert(source, ast) {
end: node.end,
expression: node.test,
children: node.consequent.nodes.map(
(child) =>
/** @type {import('./types/legacy-nodes.js').LegacyElementLike} */ (visit(child))
(child) => /** @type {Legacy.LegacyElementLike} */ (visit(child))
),
else: elseblock,
elseif: node.elseif ? true : undefined
@ -407,12 +399,10 @@ export function convert(source, ast) {
end: node.end,
name: node.name,
attributes: node.attributes.map(
(child) =>
/** @type {import('./types/legacy-nodes.js').LegacyAttributeLike} */ (visit(child))
(child) => /** @type {Legacy.LegacyAttributeLike} */ (visit(child))
),
children: node.fragment.nodes.map(
(child) =>
/** @type {import('./types/legacy-nodes.js').LegacyElementLike} */ (visit(child))
(child) => /** @type {Legacy.LegacyElementLike} */ (visit(child))
)
};
},
@ -433,12 +423,10 @@ export function convert(source, ast) {
start: node.start,
end: node.end,
attributes: node.attributes.map(
(child) =>
/** @type {import('./types/legacy-nodes.js').LegacyAttributeLike} */ (visit(child))
(child) => /** @type {Legacy.LegacyAttributeLike} */ (visit(child))
),
children: node.fragment.nodes.map(
(child) =>
/** @type {import('./types/legacy-nodes.js').LegacyElementLike} */ (visit(child))
(child) => /** @type {Legacy.LegacyElementLike} */ (visit(child))
)
};
},
@ -450,12 +438,10 @@ export function convert(source, ast) {
end: node.end,
expression: node.expression,
attributes: node.attributes.map(
(child) =>
/** @type {import('./types/legacy-nodes.js').LegacyAttributeLike} */ (visit(child))
(child) => /** @type {Legacy.LegacyAttributeLike} */ (visit(child))
),
children: node.fragment.nodes.map(
(child) =>
/** @type {import('./types/legacy-nodes.js').LegacyElementLike} */ (visit(child))
(child) => /** @type {Legacy.LegacyElementLike} */ (visit(child))
)
};
},
@ -466,17 +452,15 @@ export function convert(source, ast) {
start: node.start,
end: node.end,
attributes: node.attributes.map(
(child) =>
/** @type {import('./types/legacy-nodes.js').LegacyAttributeLike} */ (visit(child))
(child) => /** @type {Legacy.LegacyAttributeLike} */ (visit(child))
),
children: node.fragment.nodes.map(
(child) =>
/** @type {import('./types/legacy-nodes.js').LegacyElementLike} */ (visit(child))
(child) => /** @type {Legacy.LegacyElementLike} */ (visit(child))
)
};
},
SvelteElement(node, { visit }) {
/** @type {import('estree').Expression | string} */
/** @type {Expression | string} */
let tag = node.tag;
if (
tag.type === 'Literal' &&
@ -503,11 +487,10 @@ export function convert(source, ast) {
start: node.start,
end: node.end,
attributes: node.attributes.map(
(a) => /** @type {import('./types/legacy-nodes.js').LegacyAttributeLike} */ (visit(a))
(a) => /** @type {Legacy.LegacyAttributeLike} */ (visit(a))
),
children: node.fragment.nodes.map(
(child) =>
/** @type {import('./types/legacy-nodes.js').LegacyElementLike} */ (visit(child))
(child) => /** @type {Legacy.LegacyElementLike} */ (visit(child))
)
};
},
@ -518,12 +501,10 @@ export function convert(source, ast) {
start: node.start,
end: node.end,
attributes: node.attributes.map(
(child) =>
/** @type {import('./types/legacy-nodes.js').LegacyAttributeLike} */ (visit(child))
(child) => /** @type {Legacy.LegacyAttributeLike} */ (visit(child))
),
children: node.fragment.nodes.map(
(child) =>
/** @type {import('./types/legacy-nodes.js').LegacyElementLike} */ (visit(child))
(child) => /** @type {Legacy.LegacyElementLike} */ (visit(child))
)
};
},
@ -534,8 +515,7 @@ export function convert(source, ast) {
start: node.start,
end: node.end,
attributes: node.attributes.map(
(child) =>
/** @type {import('./types/legacy-nodes.js').LegacyAttributeLike} */ (visit(child))
(child) => /** @type {Legacy.LegacyAttributeLike} */ (visit(child))
)
};
},
@ -546,12 +526,10 @@ export function convert(source, ast) {
start: node.start,
end: node.end,
attributes: node.attributes.map(
(child) =>
/** @type {import('./types/legacy-nodes.js').LegacyAttributeLike} */ (visit(child))
(child) => /** @type {Legacy.LegacyAttributeLike} */ (visit(child))
),
children: node.fragment.nodes.map(
(child) =>
/** @type {import('./types/legacy-nodes.js').LegacyElementLike} */ (visit(child))
(child) => /** @type {Legacy.LegacyElementLike} */ (visit(child))
)
};
},
@ -562,12 +540,10 @@ export function convert(source, ast) {
start: node.start,
end: node.end,
attributes: node.attributes.map(
(child) =>
/** @type {import('./types/legacy-nodes.js').LegacyAttributeLike} */ (visit(child))
(child) => /** @type {Legacy.LegacyAttributeLike} */ (visit(child))
),
children: node.fragment.nodes.map(
(child) =>
/** @type {import('./types/legacy-nodes.js').LegacyElementLike} */ (visit(child))
(child) => /** @type {Legacy.LegacyElementLike} */ (visit(child))
)
};
},
@ -575,7 +551,7 @@ export function convert(source, ast) {
const parent = path.at(-1);
if (parent?.type === 'RegularElement' && parent.name === 'style') {
// these text nodes are missing `raw` for some dumb reason
return /** @type {import('./types/template.js').Text} */ ({
return /** @type {Text} */ ({
type: 'Text',
start: node.start,
end: node.end,
@ -590,12 +566,10 @@ export function convert(source, ast) {
start: node.start,
end: node.end,
attributes: node.attributes.map(
(child) =>
/** @type {import('./types/legacy-nodes.js').LegacyAttributeLike} */ (visit(child))
(child) => /** @type {Legacy.LegacyAttributeLike} */ (visit(child))
),
children: node.fragment.nodes.map(
(child) =>
/** @type {import('./types/legacy-nodes.js').LegacyElementLike} */ (visit(child))
(child) => /** @type {Legacy.LegacyElementLike} */ (visit(child))
)
};
},

@ -322,7 +322,10 @@ const instance_script = {
state.str.prependLeft(/** @type {number} */ (declarator.init.start), '$state(');
state.str.appendRight(/** @type {number} */ (declarator.init.end), ')');
} else {
state.str.prependLeft(/** @type {number} */ (declarator.id.end), ' = $state()');
state.str.prependLeft(
/** @type {number} */ (declarator.id.typeAnnotation?.end ?? declarator.id.end),
' = $state()'
);
}
}
@ -587,13 +590,13 @@ function extract_type_and_comment(declarator, str, path) {
}
/**
* @param {import('#compiler').RegularElement | import('#compiler').SvelteElement | import('#compiler').SvelteWindow | import('#compiler').SvelteDocument | import('#compiler').SvelteBody} node
* @param {import('#compiler').RegularElement | import('#compiler').SvelteElement | import('#compiler').SvelteWindow | import('#compiler').SvelteDocument | import('#compiler').SvelteBody} element
* @param {State} state
*/
function handle_events(node, state) {
function handle_events(element, state) {
/** @type {Map<string, import('#compiler').OnDirective[]>} */
const handlers = new Map();
for (const attribute of node.attributes) {
for (const attribute of element.attributes) {
if (attribute.type !== 'OnDirective') continue;
let name = `on${attribute.name}`;
@ -625,6 +628,7 @@ function handle_events(node, state) {
for (let i = 0; i < nodes.length - 1; i += 1) {
const node = nodes[i];
const indent = get_indent(state, node, element);
if (node.expression) {
let body = '';
if (node.expression.type === 'ArrowFunctionExpression') {
@ -638,19 +642,20 @@ function handle_events(node, state) {
/** @type {number} */ (node.expression.end)
)}();`;
}
// TODO check how many indents needed
for (const modifier of node.modifiers) {
if (modifier === 'stopPropagation') {
body = `\n${state.indent}${payload_name}.stopPropagation();\n${body}`;
body = `\n${indent}${payload_name}.stopPropagation();\n${body}`;
} else if (modifier === 'preventDefault') {
body = `\n${state.indent}${payload_name}.preventDefault();\n${body}`;
body = `\n${indent}${payload_name}.preventDefault();\n${body}`;
} else if (modifier === 'stopImmediatePropagation') {
body = `\n${state.indent}${payload_name}.stopImmediatePropagation();\n${body}`;
body = `\n${indent}${payload_name}.stopImmediatePropagation();\n${body}`;
} else {
body = `\n${state.indent}// @migration-task: incorporate ${modifier} modifier\n${body}`;
body = `\n${indent}// @migration-task: incorporate ${modifier} modifier\n${body}`;
}
}
prepend += `\n${state.indent}${body}\n`;
prepend += `\n${indent}${body}\n`;
} else {
if (!local) {
local = state.scope.generate(`on${node.name}`);
@ -663,7 +668,7 @@ function handle_events(node, state) {
type: '(event: any) => void'
});
}
prepend += `\n${state.indent}${local}?.(${payload_name});\n`;
prepend += `\n${indent}${local}?.(${payload_name});\n`;
}
state.str.remove(node.start, node.end);
@ -683,15 +688,17 @@ function handle_events(node, state) {
state.str.appendRight(last.start + last.name.length + 3, 'capture');
}
const indent = get_indent(state, last, element);
for (const modifier of last.modifiers) {
if (modifier === 'stopPropagation') {
prepend += `\n${state.indent}${payload_name}.stopPropagation();\n`;
prepend += `\n${indent}${payload_name}.stopPropagation();\n`;
} else if (modifier === 'preventDefault') {
prepend += `\n${state.indent}${payload_name}.preventDefault();\n`;
prepend += `\n${indent}${payload_name}.preventDefault();\n`;
} else if (modifier === 'stopImmediatePropagation') {
prepend += `\n${state.indent}${payload_name}.stopImmediatePropagation();\n`;
prepend += `\n${indent}${payload_name}.stopImmediatePropagation();\n`;
} else if (modifier !== 'capture') {
prepend += `\n${state.indent}// @migration-task: incorporate ${modifier} modifier\n`;
prepend += `\n${indent}// @migration-task: incorporate ${modifier} modifier\n`;
}
}
@ -723,17 +730,20 @@ function handle_events(node, state) {
pos = /** @type {number} */ (pos) + (needs_curlies ? 0 : 1);
if (needs_curlies && state.str.original[pos - 1] === '(') {
// Prettier does something like on:click={() => (foo = true)}, we need to remove the braces in this case
state.str.update(pos - 1, pos, `{${prepend}${state.indent}`);
state.str.update(end, end + 1, '\n}');
state.str.update(pos - 1, pos, `{${prepend}${indent}`);
state.str.update(end, end + 1, `\n${indent.slice(state.indent.length)}}`);
} else {
state.str.prependRight(pos, `${needs_curlies ? '{' : ''}${prepend}${state.indent}`);
state.str.appendRight(end, `\n${needs_curlies ? '}' : ''}`);
state.str.prependRight(pos, `${needs_curlies ? '{' : ''}${prepend}${indent}`);
state.str.appendRight(
end,
`\n${indent.slice(state.indent.length)}${needs_curlies ? '}' : ''}`
);
}
} else {
state.str.update(
/** @type {number} */ (last.expression.start),
/** @type {number} */ (last.expression.end),
`(${payload_name}) => {${prepend}\n${state.indent}${state.str.original.substring(
`(${payload_name}) => {${prepend}\n${indent}${state.str.original.substring(
/** @type {number} */ (last.expression.start),
/** @type {number} */ (last.expression.end)
)}?.(${payload_name});\n}`
@ -771,6 +781,29 @@ function handle_events(node, state) {
}
}
/**
* Returns the next indentation level of the first node that has all-whitespace before it
* @param {State} state
* @param {Array<{start: number; end: number}>} nodes
*/
function get_indent(state, ...nodes) {
let indent = state.indent;
for (const node of nodes) {
const line_start = state.str.original.lastIndexOf('\n', node.start);
indent = state.str.original.substring(line_start + 1, node.start);
if (indent.trim() === '') {
indent = state.indent + indent;
return indent;
} else {
indent = state.indent;
}
}
return indent;
}
/**
* @param {import('#compiler').OnDirective} last
* @param {State} state

@ -1,6 +1,7 @@
import * as acorn from 'acorn';
import { walk } from 'zimmerframe';
import { tsPlugin } from 'acorn-typescript';
import { locator } from '../../state.js';
const ParserWithTS = acorn.Parser.extend(tsPlugin({ allowSatisfies: true }));
@ -127,7 +128,20 @@ function amend(source, node) {
// @ts-expect-error
delete node.loc.end.index;
if (/** @type {any} */ (node).typeAnnotation && node.end === undefined) {
if (typeof node.loc?.end === 'number') {
const loc = locator(node.loc.end);
if (loc) {
node.loc.end = {
line: loc.line,
column: loc.column
};
}
}
if (
/** @type {any} */ (node).typeAnnotation &&
(node.end === undefined || node.end < node.start)
) {
// i think there might be a bug in acorn-typescript that prevents
// `end` from being assigned when there's a type annotation
let end = /** @type {any} */ (node).typeAnnotation.start;

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

@ -3,6 +3,7 @@ import read_expression from '../read/expression.js';
import * as e from '../../../errors.js';
import { create_fragment } from '../utils/create.js';
import { walk } from 'zimmerframe';
import { parse_expression_at } from '../acorn.js';
const regex_whitespace_with_closing_curly_brace = /^\s*}/;
@ -268,37 +269,28 @@ function open(parser) {
e.expected_identifier(parser.index);
}
parser.eat('(', true);
parser.allow_whitespace();
/** @type {import('estree').Pattern[]} */
const parameters = [];
while (!parser.match(')')) {
let pattern = read_pattern(parser, true);
parser.allow_whitespace();
if (parser.eat('=')) {
parser.allow_whitespace();
const right = read_expression(parser);
pattern = {
type: 'AssignmentPattern',
left: pattern,
right: right,
start: pattern.start,
end: right.end
};
}
const params_start = parser.index;
parameters.push(pattern);
parser.eat('(', true);
let parentheses = 1;
if (!parser.eat(',')) break;
parser.allow_whitespace();
while (parser.index < parser.template.length && (!parser.match(')') || parentheses !== 1)) {
if (parser.match('(')) parentheses++;
if (parser.match(')')) parentheses--;
parser.index += 1;
}
parser.eat(')', true);
const prelude = parser.template.slice(0, params_start).replace(/\S/g, ' ');
const params = parser.template.slice(params_start, parser.index);
let function_expression = /** @type {import('estree').ArrowFunctionExpression} */ (
parse_expression_at(prelude + `${params} => {}`, parser.ts, params_start)
);
parser.allow_whitespace();
parser.eat('}', true);
@ -313,10 +305,9 @@ function open(parser) {
end: name_end,
name
},
parameters,
parameters: function_expression.params,
body: create_fragment()
});
parser.stack.push(block);
parser.fragments.push(block.body);
@ -603,7 +594,10 @@ function special(parser) {
type: 'RenderTag',
start,
end: parser.index,
expression: expression
expression: expression,
metadata: {
dynamic: false
}
});
}
}

@ -1086,6 +1086,8 @@ function check_element(node, state) {
if (
node.type === 'SvelteElement' ||
node.type === 'SlotElement' ||
node.type === 'Component' ||
node.type === 'RenderTag' ||
(node.type === 'RegularElement' &&
(a11y_labelable.includes(node.name) || node.name === 'slot'))
) {

@ -31,6 +31,7 @@ import { hash } from './utils.js';
import { warn_unused } from './css/css-warn.js';
import { extract_svelte_ignore } from '../../utils/extract_svelte_ignore.js';
import { ignore_map, ignore_stack, pop_ignore, push_ignore } from '../../state.js';
import { equal } from '../../utils/assert.js';
/**
* @param {import('#compiler').Script | null} script
@ -111,23 +112,24 @@ function get_delegated_event(event_name, handler, context) {
if (binding != null) {
for (const { path } of binding.references) {
const parent = path.at(-1);
if (parent == null) {
return non_hoistable;
}
if (parent == null) return non_hoistable;
const grandparent = path.at(-2);
/** @type {import('#compiler').RegularElement | null} */
let element = null;
/** @type {string | null} */
let event_name = null;
if (parent.type === 'OnDirective') {
element = /** @type {import('#compiler').RegularElement} */ (path.at(-2));
element = /** @type {import('#compiler').RegularElement} */ (grandparent);
event_name = parent.name;
} else if (
parent.type === 'ExpressionTag' &&
is_event_attribute(/** @type {import('#compiler').Attribute} */ (path.at(-2)))
grandparent?.type === 'Attribute' &&
is_event_attribute(grandparent)
) {
element = /** @type {import('#compiler').RegularElement} */ (path.at(-3));
const attribute = /** @type {import('#compiler').Attribute} */ (path.at(-2));
const attribute = /** @type {import('#compiler').Attribute} */ (grandparent);
event_name = get_attribute_event_name(attribute.name);
}
@ -968,34 +970,42 @@ const runes_scope_tweaker = {
if (rune === '$props') {
state.analysis.needs_props = true;
for (const property of /** @type {import('estree').ObjectPattern} */ (node.id).properties) {
if (property.type !== 'Property') continue;
const name =
property.value.type === 'AssignmentPattern'
? /** @type {import('estree').Identifier} */ (property.value.left).name
: /** @type {import('estree').Identifier} */ (property.value).name;
const alias =
property.key.type === 'Identifier'
? property.key.name
: String(/** @type {import('estree').Literal} */ (property.key).value);
let initial = property.value.type === 'AssignmentPattern' ? property.value.right : null;
const binding = /** @type {import('#compiler').Binding} */ (state.scope.get(name));
binding.prop_alias = alias;
// 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;
if (node.id.type === 'Identifier') {
const binding = /** @type {import('#compiler').Binding} */ (state.scope.get(node.id.name));
binding.initial = null; // else would be $props()
binding.kind = 'rest_prop';
} else {
equal(node.id.type, 'ObjectPattern');
for (const property of node.id.properties) {
if (property.type !== 'Property') continue;
const name =
property.value.type === 'AssignmentPattern'
? /** @type {import('estree').Identifier} */ (property.value.left).name
: /** @type {import('estree').Identifier} */ (property.value).name;
const alias =
property.key.type === 'Identifier'
? property.key.name
: String(/** @type {import('estree').Literal} */ (property.key).value);
let initial = property.value.type === 'AssignmentPattern' ? property.value.right : null;
const binding = /** @type {import('#compiler').Binding} */ (state.scope.get(name));
binding.prop_alias = alias;
// 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;
}
}
}
}
@ -1237,6 +1247,14 @@ const common_visitors = {
return;
}
// If we are using arguments outside of a function, then throw an error
if (
node.name === 'arguments' &&
context.path.every((n) => n.type !== 'FunctionDeclaration' && n.type !== 'FunctionExpression')
) {
e.invalid_arguments_usage(node);
}
const binding = context.state.scope.get(node.name);
// if no binding, means some global variable
@ -1502,6 +1520,13 @@ const common_visitors = {
return;
}
}
},
Component(node, context) {
const binding = context.state.scope.get(
node.name.includes('.') ? node.name.slice(0, node.name.indexOf('.')) : node.name
);
node.metadata.dynamic = binding !== null && binding.kind !== 'normal';
}
};

@ -256,8 +256,16 @@ function validate_attribute_name(attribute) {
* @param {boolean} is_component
*/
function validate_slot_attribute(context, attribute, is_component = false) {
const parent = context.path.at(-2);
let owner = undefined;
if (parent?.type === 'SnippetBlock') {
if (!is_text_attribute(attribute)) {
e.slot_attribute_invalid(attribute);
}
return;
}
let i = context.path.length;
while (i--) {
const ancestor = context.path[i];
@ -283,7 +291,7 @@ function validate_slot_attribute(context, attribute, is_component = false) {
owner.type === 'SvelteComponent' ||
owner.type === 'SvelteSelf'
) {
if (owner !== context.path.at(-2)) {
if (owner !== parent) {
e.slot_attribute_invalid_placement(attribute);
}
@ -333,6 +341,14 @@ function validate_block_not_empty(node, context) {
* @type {import('zimmerframe').Visitors<import('#compiler').SvelteNode, import('./types.js').AnalysisState>}
*/
const validation = {
MemberExpression(node, context) {
if (node.object.type === 'Identifier' && node.property.type === 'Identifier') {
const binding = context.state.scope.get(node.object.name);
if (binding?.kind === 'rest_prop' && node.property.name.startsWith('$$')) {
e.props_illegal_name(node.property);
}
}
},
AssignmentExpression(node, context) {
validate_assignment(node, node.left, context.state);
},
@ -617,6 +633,11 @@ const validation = {
});
},
RenderTag(node, context) {
const callee = unwrap_optional(node.expression).callee;
node.metadata.dynamic =
callee.type !== 'Identifier' || context.state.scope.get(callee.name)?.kind !== 'normal';
context.state.analysis.uses_render_tags = true;
const raw_args = unwrap_optional(node.expression).arguments;
@ -626,7 +647,6 @@ const validation = {
}
}
const callee = unwrap_optional(node.expression).callee;
if (
callee.type === 'MemberExpression' &&
callee.property.type === 'Identifier' &&
@ -1232,7 +1252,7 @@ export const validation_runes = merge(validation, a11y_validators, {
e.rune_invalid_arguments(node, rune);
}
if (node.id.type !== 'ObjectPattern') {
if (node.id.type !== 'ObjectPattern' && node.id.type !== 'Identifier') {
e.props_invalid_identifier(node);
}
@ -1240,17 +1260,23 @@ export const validation_runes = merge(validation, a11y_validators, {
e.props_invalid_placement(node);
}
for (const property of node.id.properties) {
if (property.type === 'Property') {
if (property.computed) {
e.props_invalid_pattern(property);
}
if (node.id.type === 'ObjectPattern') {
for (const property of node.id.properties) {
if (property.type === 'Property') {
if (property.computed) {
e.props_invalid_pattern(property);
}
if (property.key.type === 'Identifier' && property.key.name.startsWith('$$')) {
e.props_illegal_name(property);
}
const value =
property.value.type === 'AssignmentPattern' ? property.value.left : property.value;
const value =
property.value.type === 'AssignmentPattern' ? property.value.left : property.value;
if (value.type !== 'Identifier') {
e.props_invalid_pattern(property);
if (value.type !== 'Identifier') {
e.props_invalid_pattern(property);
}
}
}
}

@ -363,7 +363,12 @@ export function client_component(source, analysis, options) {
}
if (analysis.uses_props || analysis.uses_rest_props) {
const to_remove = [b.literal('children'), b.literal('$$slots'), b.literal('$$events')];
const to_remove = [
b.literal('children'),
b.literal('$$slots'),
b.literal('$$events'),
b.literal('$$legacy')
];
if (analysis.custom_element) {
to_remove.push(b.literal('$$host'));
}

@ -1,5 +1,6 @@
import * as b from '../../../utils/builders.js';
import {
extract_identifiers,
extract_paths,
is_expression_async,
is_simple_expression,
@ -88,7 +89,7 @@ export function serialize_get_binding(node, state) {
}
if (binding.kind === 'prop' || binding.kind === 'bindable_prop') {
if (!state.analysis.runes || binding.reassigned || binding.initial) {
if (is_prop_source(binding, state)) {
return b.call(node);
}
@ -119,10 +120,11 @@ export function serialize_get_binding(node, state) {
* @param {import('estree').AssignmentExpression} node
* @param {import('zimmerframe').Context<import('#compiler').SvelteNode, State>} context
* @param {() => any} fallback
* @param {boolean | null} [prefix] - If the assignment is a transformed update expression, set this. Else `null`
* @param {{skip_proxy_and_freeze?: boolean}} [options]
* @returns {import('estree').Expression}
*/
export function serialize_set_binding(node, context, fallback, options) {
export function serialize_set_binding(node, context, fallback, prefix, options) {
const { state, visit } = context;
const assignee = node.left;
@ -146,7 +148,9 @@ export function serialize_set_binding(node, context, fallback, options) {
const value = path.expression?.(b.id(tmp_id));
const assignment = b.assignment('=', path.node, value);
original_assignments.push(assignment);
assignments.push(serialize_set_binding(assignment, context, () => assignment, options));
assignments.push(
serialize_set_binding(assignment, context, () => assignment, prefix, options)
);
}
if (assignments.every((assignment, i) => assignment === original_assignments[i])) {
@ -387,18 +391,20 @@ export function serialize_set_binding(node, context, fallback, options) {
),
b.call('$.untrack', b.id('$' + left_name))
);
} else if (!state.analysis.runes) {
} else if (
!state.analysis.runes ||
// this condition can go away once legacy mode is gone; only necessary for interop with legacy parent bindings
(binding.mutated && binding.kind === 'bindable_prop')
) {
if (binding.kind === 'bindable_prop') {
return b.call(
left,
b.sequence([
b.assignment(
node.operator,
/** @type {import('estree').Pattern} */ (visit(node.left)),
value
),
b.call(left)
])
b.assignment(
node.operator,
/** @type {import('estree').Pattern} */ (visit(node.left)),
value
),
b.true
);
} else {
return b.call(
@ -411,6 +417,16 @@ export function serialize_set_binding(node, context, fallback, options) {
)
);
}
} else if (
node.right.type === 'Literal' &&
prefix != null &&
(node.operator === '+=' || node.operator === '-=')
) {
return b.update(
node.operator === '+=' ? '++' : '--',
/** @type {import('estree').Expression} */ (visit(node.left)),
prefix
);
} else {
return b.assignment(
node.operator,
@ -525,9 +541,7 @@ function get_hoistable_params(node, context) {
} else if (
// If we are referencing a simple $$props value, then we need to reference the object property instead
(binding.kind === 'prop' || binding.kind === 'bindable_prop') &&
!binding.reassigned &&
binding.initial === null &&
!context.state.analysis.accessors
!is_prop_source(binding, context.state)
) {
push_unique(b.id('$$props'));
} else {
@ -589,7 +603,9 @@ export function get_prop_source(binding, state, name, initial) {
if (
state.analysis.accessors ||
(state.analysis.immutable ? binding.reassigned : binding.mutated)
(state.analysis.immutable
? binding.reassigned || (state.analysis.runes && binding.mutated)
: binding.mutated)
) {
flags |= PROPS_IS_UPDATED;
}
@ -624,6 +640,25 @@ export function get_prop_source(binding, state, name, initial) {
return b.call('$.prop', ...args);
}
/**
*
* @param {import('#compiler').Binding} binding
* @param {import('./types').ClientTransformState} state
* @returns
*/
export function is_prop_source(binding, state) {
return (
(binding.kind === 'prop' || binding.kind === 'bindable_prop') &&
(!state.analysis.runes ||
state.analysis.accessors ||
binding.reassigned ||
binding.initial ||
// Until legacy mode is gone, we also need to use the prop source when only mutated is true,
// because the parent could be a legacy component which needs coarse-grained reactivity
binding.mutated)
);
}
/**
* @param {import('estree').Expression} node
* @param {import("../../scope.js").Scope | null} scope
@ -672,3 +707,44 @@ export function with_loc(target, source) {
}
return target;
}
/**
* @param {import("estree").Pattern} node
* @param {import("zimmerframe").Context<import("#compiler").SvelteNode, import("./types").ComponentClientTransformState>} context
* @returns {{ id: import("estree").Pattern, declarations: null | import("estree").Statement[] }}
*/
export function create_derived_block_argument(node, context) {
if (node.type === 'Identifier') {
return { id: node, declarations: null };
}
const pattern = /** @type {import('estree').Pattern} */ (context.visit(node));
const identifiers = extract_identifiers(node);
const id = b.id('$$source');
const value = b.id('$$value');
const block = b.block([
b.var(pattern, b.call('$.get', id)),
b.return(b.object(identifiers.map((identifier) => b.prop('init', identifier, identifier))))
]);
const declarations = [b.var(value, create_derived(context.state, b.thunk(block)))];
for (const id of identifiers) {
declarations.push(
b.var(id, create_derived(context.state, b.thunk(b.member(b.call('$.get', value), id))))
);
}
return { id, declarations };
}
/**
* Svelte legacy mode should use safe equals in most places, runes mode shouldn't
* @param {import('./types.js').ComponentClientTransformState} state
* @param {import('estree').Expression} arg
*/
export function create_derived(state, arg) {
return b.call(state.analysis.runes ? '$.derived' : '$.derived_safe_equal', arg);
}

@ -9,6 +9,27 @@ export const global_visitors = {
if (node.name === '$$props') {
return b.id('$$sanitized_props');
}
// Optimize prop access: If it's a member read access, we can use the $$props object directly
const binding = state.scope.get(node.name);
if (
state.analysis.runes && // can't do this in legacy mode because the proxy does more than just read/write
binding !== null &&
node !== binding.node &&
binding.kind === 'rest_prop'
) {
const parent = path.at(-1);
const grand_parent = path.at(-2);
if (
parent?.type === 'MemberExpression' &&
!parent.computed &&
grand_parent?.type !== 'AssignmentExpression' &&
grand_parent?.type !== 'UpdateExpression'
) {
return b.id('$$props');
}
}
return serialize_get_binding(node, state);
}
},
@ -98,7 +119,12 @@ export const global_visitors = {
/** @type {import('estree').Pattern} */ (argument),
b.literal(1)
);
const serialized_assignment = serialize_set_binding(assignment, context, () => assignment);
const serialized_assignment = serialize_set_binding(
assignment,
context,
() => assignment,
node.prefix
);
const value = /** @type {import('estree').Expression} */ (visit(argument));
if (serialized_assignment === assignment) {
// No change to output -> nothing to transform -> we can keep the original update expression

@ -4,6 +4,7 @@ import * as b from '../../../../utils/builders.js';
import * as assert from '../../../../utils/assert.js';
import {
get_prop_source,
is_prop_source,
is_state_source,
serialize_proxy_reassignment,
should_proxy_or_freeze
@ -237,49 +238,65 @@ export const javascript_visitors_runes = {
}
if (rune === '$props') {
assert.equal(declarator.id.type, 'ObjectPattern');
/** @type {string[]} */
const seen = [];
const seen = ['$$slots', '$$events', '$$legacy'];
for (const property of declarator.id.properties) {
if (property.type === 'Property') {
const key = /** @type {import('estree').Identifier | import('estree').Literal} */ (
property.key
);
const name = key.type === 'Identifier' ? key.name : /** @type {string} */ (key.value);
seen.push(name);
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));
let initial =
binding.initial &&
/** @type {import('estree').Expression} */ (visit(binding.initial));
// We're adding proxy here on demand and not within the prop runtime function so that
// people not using proxied state anywhere in their code don't have to pay the additional bundle size cost
if (initial && binding.mutated && should_proxy_or_freeze(initial, state.scope)) {
initial = b.call('$.proxy', initial);
}
if (state.analysis.custom_element) {
seen.push('$$host');
}
if (binding.reassigned || state.analysis.accessors || initial) {
declarations.push(b.declarator(id, get_prop_source(binding, state, name, initial)));
}
} else {
// RestElement
/** @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)
if (declarator.id.type === 'Identifier') {
/** @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(declarator.id.name));
}
declarations.push(b.declarator(declarator.id, b.call('$.rest_props', ...args)));
} else {
assert.equal(declarator.id.type, 'ObjectPattern');
for (const property of declarator.id.properties) {
if (property.type === 'Property') {
const key = /** @type {import('estree').Identifier | import('estree').Literal} */ (
property.key
);
const name = key.type === 'Identifier' ? key.name : /** @type {string} */ (key.value);
seen.push(name);
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));
let initial =
binding.initial &&
/** @type {import('estree').Expression} */ (visit(binding.initial));
// We're adding proxy here on demand and not within the prop runtime function so that
// people not using proxied state anywhere in their code don't have to pay the additional bundle size cost
if (initial && binding.mutated && should_proxy_or_freeze(initial, state.scope)) {
initial = b.call('$.proxy', initial);
}
if (is_prop_source(binding, state)) {
declarations.push(b.declarator(id, get_prop_source(binding, state, name, initial)));
}
} else {
// RestElement
/** @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)));
}
declarations.push(b.declarator(property.argument, b.call('$.rest_props', ...args)));
}
}

@ -21,7 +21,9 @@ import {
function_visitor,
get_assignment_value,
serialize_get_binding,
serialize_set_binding
serialize_set_binding,
create_derived,
create_derived_block_argument
} from '../utils.js';
import {
AttributeAliases,
@ -34,6 +36,7 @@ import {
EACH_KEYED,
is_capture_event,
TEMPLATE_FRAGMENT,
TEMPLATE_UNSET_START,
TEMPLATE_USE_IMPORT_NODE,
TRANSITION_GLOBAL,
TRANSITION_IN,
@ -646,15 +649,6 @@ function collect_parent_each_blocks(context) {
);
}
/**
* Svelte legacy mode should use safe equals in most places, runes mode shouldn't
* @param {import('../types.js').ComponentClientTransformState} state
* @param {import('estree').Expression} arg
*/
function create_derived(state, arg) {
return b.call(state.analysis.runes ? '$.derived' : '$.derived_safe_equal', arg);
}
/**
* @param {import('#compiler').Component | import('#compiler').SvelteComponent | import('#compiler').SvelteSelf} node
* @param {string} component_name
@ -886,7 +880,12 @@ function serialize_inline_component(node, component_name, context) {
if (slot_name === 'default' && !has_children_prop) {
push_prop(
b.init('children', context.state.options.dev ? b.call('$.wrap_snippet', slot_fn) : slot_fn)
b.init(
'children',
context.state.options.dev
? b.call('$.wrap_snippet', slot_fn, b.id(context.state.analysis.name))
: slot_fn
)
);
// We additionally add the default slot as a boolean, so that the slot render function on the other
// side knows it should get the content to render from $$props.children
@ -900,6 +899,10 @@ function serialize_inline_component(node, component_name, context) {
push_prop(b.init('$$slots', b.object(serialized_slots)));
}
if (!context.state.analysis.runes) {
push_prop(b.init('$$legacy', b.true));
}
const props_expression =
props_and_spreads.length === 0 ||
(props_and_spreads.length === 1 && Array.isArray(props_and_spreads[0]))
@ -940,6 +943,7 @@ function serialize_inline_component(node, component_name, context) {
fn = (node_id) => {
return b.call(
'$.component',
node_id,
b.thunk(/** @type {import('estree').Expression} */ (context.visit(node.expression))),
b.arrow(
[b.id(component_name)],
@ -1094,13 +1098,12 @@ function serialize_update(statement) {
}
/**
*
* @param {import('../types.js').ComponentClientTransformState} state
* @param {import('estree').Statement[]} update
*/
function serialize_render_stmt(state) {
return state.update.length === 1
? serialize_update(state.update[0])
: b.stmt(b.call('$.template_effect', b.thunk(b.block(state.update))));
function serialize_render_stmt(update) {
return update.length === 1
? serialize_update(update[0])
: b.stmt(b.call('$.template_effect', b.thunk(b.block(update))));
}
/**
@ -1678,14 +1681,35 @@ export const template_visitors = {
process_children(trimmed, expression, false, { ...context, state });
var first = trimmed[0];
/**
* If the first item in an effect is a static slot or render tag, it will clone
* a template but without creating a child effect. In these cases, we need to keep
* the current `effect.nodes.start` undefined, so that it can be populated by
* the item in question
* TODO come up with a better name than `unset`
*/
var unset = false;
if (first.type === 'SlotElement') unset = true;
if (first.type === 'RenderTag' && !first.metadata.dynamic) unset = true;
if (first.type === 'Component' && !first.metadata.dynamic && !context.state.options.hmr) {
unset = true;
}
const use_comment_template = state.template.length === 1 && state.template[0] === '<!>';
if (use_comment_template) {
// special case — we can use `$.comment` instead of creating a unique template
body.push(b.var(id, b.call('$.comment')));
body.push(b.var(id, b.call('$.comment', unset && b.literal(unset))));
} else {
let flags = TEMPLATE_FRAGMENT;
if (unset) {
flags |= TEMPLATE_UNSET_START;
}
if (state.metadata.context.template_needs_import_node) {
flags |= TEMPLATE_USE_IMPORT_NODE;
}
@ -1707,7 +1731,7 @@ export const template_visitors = {
}
if (state.update.length > 0) {
body.push(serialize_render_stmt(state));
body.push(serialize_render_stmt(state.update));
}
body.push(...state.after_update);
@ -1830,27 +1854,26 @@ export const template_visitors = {
context.state.template.push('<!>');
const callee = unwrap_optional(node.expression).callee;
const raw_args = unwrap_optional(node.expression).arguments;
const is_reactive =
callee.type !== 'Identifier' || context.state.scope.get(callee.name)?.kind !== 'normal';
/** @type {import('estree').Expression[]} */
const args = [context.state.node];
for (const arg of raw_args) {
args.push(b.thunk(/** @type {import('estree').Expression} */ (context.visit(arg))));
}
const args = raw_args.map((arg) =>
b.thunk(/** @type {import('estree').Expression} */ (context.visit(arg)))
);
let snippet_function = /** @type {import('estree').Expression} */ (context.visit(callee));
if (context.state.options.dev) {
snippet_function = b.call('$.validate_snippet', snippet_function);
}
if (is_reactive) {
context.state.init.push(b.stmt(b.call('$.snippet', b.thunk(snippet_function), ...args)));
if (node.metadata.dynamic) {
context.state.init.push(
b.stmt(b.call('$.snippet', context.state.node, b.thunk(snippet_function), ...args))
);
} else {
context.state.init.push(
b.stmt(
(node.expression.type === 'CallExpression' ? b.call : b.maybe_call)(
snippet_function,
context.state.node,
...args
)
)
@ -1913,7 +1936,7 @@ export const template_visitors = {
}
if (node.name === 'noscript') {
context.state.template.push('<!>');
context.state.template.push('<noscript></noscript>');
return;
}
if (node.name === 'script') {
@ -1953,6 +1976,7 @@ export const template_visitors = {
let has_content_editable_binding = false;
let img_might_be_lazy = false;
let might_need_event_replaying = false;
let has_direction_attribute = false;
if (is_custom_element) {
// cloneNode is faster, but it does not instantiate the underlying class of the
@ -1968,6 +1992,9 @@ export const template_visitors = {
if (node.name === 'img' && attribute.name === 'loading') {
img_might_be_lazy = true;
}
if (attribute.name === 'dir') {
has_direction_attribute = true;
}
if (
(attribute.name === 'value' || attribute.name === 'checked') &&
!is_text_attribute(attribute)
@ -2030,7 +2057,7 @@ export const template_visitors = {
}
if (needs_input_reset && node.name === 'input') {
context.state.init.push(b.stmt(b.call('$.remove_input_attr_defaults', context.state.node)));
context.state.init.push(b.stmt(b.call('$.remove_input_defaults', context.state.node)));
}
if (needs_content_reset && node.name === 'textarea') {
@ -2151,8 +2178,15 @@ export const template_visitors = {
state.options.preserveComments
);
/** Whether or not we need to wrap the children in `{...}` to avoid declaration conflicts */
const has_declaration = node.fragment.nodes.some((node) => node.type === 'SnippetBlock');
const child_state = has_declaration
? { ...state, init: [], update: [], after_update: [] }
: state;
for (const node of hoisted) {
context.visit(node, state);
context.visit(node, child_state);
}
process_children(
@ -2165,9 +2199,27 @@ export const template_visitors = {
: context.state.node
),
true,
{ ...context, state }
{ ...context, state: child_state }
);
if (has_declaration) {
context.state.init.push(
b.block([
...child_state.init,
child_state.update.length > 0 ? serialize_render_stmt(child_state.update) : b.empty,
...child_state.after_update
])
);
}
if (has_direction_attribute) {
// This fixes an issue with Chromium where updates to text content within an element
// does not update the direction when set to auto. If we just re-assign the dir, this fixes it.
context.state.update.push(
b.stmt(b.assignment('=', b.member(node_id, b.id('dir')), b.member(node_id, b.id('dir'))))
);
}
if (child_locations.length > 0) {
// @ts-expect-error
location.push(child_locations);
@ -2256,7 +2308,7 @@ export const template_visitors = {
/** @type {import('estree').Statement[]} */
const inner = inner_context.state.init;
if (inner_context.state.update.length > 0) {
inner.push(serialize_render_stmt(inner_context.state));
inner.push(serialize_render_stmt(inner_context.state.update));
}
inner.push(...inner_context.state.after_update);
inner.push(
@ -2433,7 +2485,7 @@ export const template_visitors = {
: b.id(node.index);
const item = each_node_meta.item;
const binding = /** @type {import('#compiler').Binding} */ (context.state.scope.get(item.name));
binding.expression = (id) => {
binding.expression = (/** @type {import("estree").Identifier} */ id) => {
const item_with_loc = with_loc(item, id);
return b.call('$.unwrap', item_with_loc);
};
@ -2587,6 +2639,45 @@ export const template_visitors = {
AwaitBlock(node, context) {
context.state.template.push('<!>');
let then_block;
let catch_block;
if (node.then) {
/** @type {import('estree').Pattern[]} */
const args = [b.id('$$anchor')];
const block = /** @type {import('estree').BlockStatement} */ (context.visit(node.then));
if (node.value) {
const argument = create_derived_block_argument(node.value, context);
args.push(argument.id);
if (argument.declarations !== null) {
block.body.unshift(...argument.declarations);
}
}
then_block = b.arrow(args, block);
}
if (node.catch) {
/** @type {import('estree').Pattern[]} */
const args = [b.id('$$anchor')];
const block = /** @type {import('estree').BlockStatement} */ (context.visit(node.catch));
if (node.error) {
const argument = create_derived_block_argument(node.error, context);
args.push(argument.id);
if (argument.declarations !== null) {
block.body.unshift(...argument.declarations);
}
}
catch_block = b.arrow(args, block);
}
context.state.init.push(
b.stmt(
b.call(
@ -2599,28 +2690,8 @@ export const template_visitors = {
/** @type {import('estree').BlockStatement} */ (context.visit(node.pending))
)
: b.literal(null),
node.then
? b.arrow(
node.value
? [
b.id('$$anchor'),
/** @type {import('estree').Pattern} */ (context.visit(node.value))
]
: [b.id('$$anchor')],
/** @type {import('estree').BlockStatement} */ (context.visit(node.then))
)
: b.literal(null),
node.catch
? b.arrow(
node.error
? [
b.id('$$anchor'),
/** @type {import('estree').Pattern} */ (context.visit(node.error))
]
: [b.id('$$anchor')],
/** @type {import('estree').BlockStatement} */ (context.visit(node.catch))
)
: b.literal(null)
then_block,
catch_block
)
)
);
@ -2699,10 +2770,10 @@ export const template_visitors = {
let snippet = b.arrow(args, body);
if (context.state.options.dev) {
snippet = b.call('$.wrap_snippet', snippet);
snippet = b.call('$.wrap_snippet', snippet, b.id(context.state.analysis.name));
}
const declaration = b.var(node.expression, snippet);
const declaration = b.const(node.expression, snippet);
// Top-level snippets are hoisted so they can be referenced in the `<script>`
if (context.path.length === 1 && context.path[0].type === 'Fragment') {
@ -2757,6 +2828,7 @@ export const template_visitors = {
assignment,
context,
() => /** @type {import('estree').Expression} */ (visit(assignment)),
null,
{
skip_proxy_and_freeze: true
}
@ -2951,16 +3023,14 @@ export const template_visitors = {
}
},
Component(node, context) {
const binding = context.state.scope.get(
node.name.includes('.') ? node.name.slice(0, node.name.indexOf('.')) : node.name
);
if (binding !== null && binding.kind !== 'normal') {
if (node.metadata.dynamic) {
// Handle dynamic references to what seems like static inline components
const component = serialize_inline_component(node, '$$component', context);
context.state.init.push(
b.stmt(
b.call(
'$.component',
context.state.node,
// TODO use untrack here to not update when binding changes?
// Would align with Svelte 4 behavior, but it's arguably nicer/expected to update this
b.thunk(
@ -2972,6 +3042,7 @@ export const template_visitors = {
);
return;
}
const component = serialize_inline_component(node, node.name, context);
context.state.init.push(component);
},

@ -613,7 +613,7 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
scopes.set(node.value, value_scope);
context.visit(node.value, { scope: value_scope });
for (const id of extract_identifiers(node.value)) {
then_scope.declare(id, 'normal', 'const');
then_scope.declare(id, 'derived', 'const');
value_scope.declare(id, 'normal', 'const');
}
}
@ -627,7 +627,7 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
scopes.set(node.error, error_scope);
context.visit(node.error, { scope: error_scope });
for (const id of extract_identifiers(node.error)) {
catch_scope.declare(id, 'normal', 'const');
catch_scope.declare(id, 'derived', 'const');
error_scope.declare(id, 'normal', 'const');
}
}

@ -69,7 +69,7 @@ export interface PreprocessorGroup {
}
/**
* Utility type to extract the type of a preprocessor from a preprocessor group
* @description Utility type to extract the type of a preprocessor from a preprocessor group
* @deprecated Create this utility type yourself instead
*/
export interface SveltePreprocessor<

@ -1,5 +1,8 @@
export type { Preprocessor, PreprocessorGroup } from './preprocess/public';
export type { CompileOptions } from './types/index';
export * from './index.js';
export type {
MarkupPreprocessor,
Preprocessor,
PreprocessorGroup,
Processed
} from './preprocess/public';
export type { CompileOptions, ModuleCompileOptions, CompileResult, Warning } from './types/index';

@ -152,6 +152,9 @@ export interface DebugTag extends BaseNode {
export interface RenderTag extends BaseNode {
type: 'RenderTag';
expression: SimpleCallExpression | (ChainExpression & { expression: SimpleCallExpression });
metadata: {
dynamic: boolean;
};
}
type Tag = ExpressionTag | HtmlTag | ConstTag | DebugTag | RenderTag;
@ -271,6 +274,9 @@ interface BaseElement extends BaseNode {
export interface Component extends BaseElement {
type: 'Component';
metadata: {
dynamic: boolean;
};
}
interface TitleElement extends BaseElement {

@ -18,6 +18,7 @@ export const TRANSITION_GLOBAL = 1 << 2;
export const TEMPLATE_FRAGMENT = 1;
export const TEMPLATE_USE_IMPORT_NODE = 1 << 1;
export const TEMPLATE_UNSET_START = 1 << 2;
export const HYDRATION_START = '[';
export const HYDRATION_END = ']';

@ -189,14 +189,31 @@ export type ComponentEvents<Comp extends SvelteComponent> =
Comp extends SvelteComponent<any, infer Events> ? Events : never;
/**
* Convenience type to get the props the given component expects. Example:
* ```html
* <script lang="ts">
* import type { ComponentProps } from 'svelte';
* import Component from './Component.svelte';
* Convenience type to get the props the given component expects.
*
* const props: ComponentProps<Component> = { foo: 'bar' }; // Errors if these aren't the correct props
* </script>
* Example: Ensure a variable contains the props expected by `MyComponent`:
*
* ```ts
* import type { ComponentProps } from 'svelte';
* import MyComponent from './MyComponent.svelte';
*
* // Errors if these aren't the correct props expected by MyComponent.
* const props: ComponentProps<MyComponent> = { foo: 'bar' };
* ```
*
* Example: A generic function that accepts some component and infers the type of its props:
*
* ```ts
* import type { Component, ComponentProps } from 'svelte';
* import MyComponent from './MyComponent.svelte';
*
* function withProps<TComponent extends Component<any>>(
* component: TComponent,
* props: ComponentProps<TComponent>
* ) {};
*
* // Errors if the second argument is not the correct props expected by the component in the first argument.
* withProps(MyComponent, { foo: 'bar' });
* ```
*/
export type ComponentProps<Comp extends SvelteComponent | Component<any>> =
@ -242,20 +259,24 @@ declare const SnippetReturn: unique symbol;
/**
* The type of a `#snippet` block. You can use it to (for example) express that your component expects a snippet of a certain type:
* ```ts
* let { banner }: { banner: Snippet<{ text: string }> } = $props();
* let { banner }: { banner: Snippet<[{ text: string }]> } = $props();
* ```
* You can only call a snippet through the `{@render ...}` tag.
*
* https://svelte-5-preview.vercel.app/docs/snippets
*
* @template Parameters the parameters that the snippet expects (if any) as a tuple.
*/
export type Snippet<T extends unknown[] = []> =
export type Snippet<Parameters extends unknown[] = []> =
// this conditional allows tuples but not arrays. Arrays would indicate a
// rest parameter type, which is not supported. If rest parameters are added
// in the future, the condition can be removed.
number extends T['length']
number extends Parameters['length']
? never
: {
(
this: void,
...args: T
...args: Parameters
): typeof SnippetReturn & {
_: 'functions passed to {@render ...} tags must use the `Snippet` type imported from "svelte"';
};

@ -16,6 +16,8 @@ export const EFFECT_RAN = 1 << 14;
export const EFFECT_TRANSPARENT = 1 << 15;
/** Svelte 4 legacy mode props need to be handled with deriveds and be recognized elsewhere, hence the dedicated flag */
export const LEGACY_DERIVED_PROP = 1 << 16;
export const INSPECT_EFFECT = 1 << 17;
export const HEAD_EFFECT = 1 << 18;
export const STATE_SYMBOL = Symbol('$state');
export const STATE_FROZEN_SYMBOL = Symbol('$state.frozen');

@ -18,7 +18,7 @@ export function hmr(source) {
/** @type {import("#client").Effect} */
let effect;
block(() => {
block(anchor, 0, () => {
const component = get(source);
if (effect) {

@ -1,20 +1,7 @@
import { DEV } from 'esm-env';
import { snapshot } from '../proxy.js';
import { render_effect, validate_effect } from '../reactivity/effects.js';
import { deep_read, untrack } from '../runtime.js';
import { inspect_effect, validate_effect } from '../reactivity/effects.js';
import { array_prototype, get_prototype_of, object_prototype } from '../utils.js';
/** @type {Function | null} */
export let inspect_fn = null;
/** @param {Function | null} fn */
export function set_inspect_fn(fn) {
inspect_fn = fn;
}
/** @type {Array<import('#client').ValueDebug>} */
export let inspect_captured_signals = [];
/**
* @param {() => any[]} get_value
* @param {Function} [inspector]
@ -25,32 +12,9 @@ export function inspect(get_value, inspector = console.log) {
let initial = true;
// we assign the function directly to signals, rather than just
// calling `inspector` directly inside the effect, so that
// we get useful stack traces
var fn = () => {
const value = untrack(() => deep_snapshot(get_value()));
inspector(initial ? 'init' : 'update', ...value);
};
render_effect(() => {
inspect_fn = fn;
deep_read(get_value());
inspect_fn = null;
const signals = inspect_captured_signals.slice();
inspect_captured_signals = [];
if (initial) {
fn();
initial = false;
}
return () => {
for (const s of signals) {
s.inspect.delete(fn);
}
};
inspect_effect(() => {
inspector(initial ? 'init' : 'update', ...deep_snapshot(get_value()));
initial = false;
});
}

@ -2,116 +2,144 @@ import { is_promise, noop } from '../../../shared/utils.js';
import {
current_component_context,
flush_sync,
is_runes,
set_current_component_context,
set_current_effect,
set_current_reaction,
set_dev_current_component_function
} from '../../runtime.js';
import { block, branch, destroy_effect, pause_effect } from '../../reactivity/effects.js';
import { INERT } from '../../constants.js';
import { block, branch, pause_effect, resume_effect } from '../../reactivity/effects.js';
import { DEV } from 'esm-env';
import { queue_micro_task } from '../task.js';
import { hydrating } from '../hydration.js';
import { mutable_source, set, source } from '../../reactivity/sources.js';
const PENDING = 0;
const THEN = 1;
const CATCH = 2;
/**
* @template V
* @param {Comment} anchor
* @param {(() => Promise<V>)} get_input
* @param {null | ((anchor: Node) => void)} pending_fn
* @param {null | ((anchor: Node, value: V) => void)} then_fn
* @param {null | ((anchor: Node, value: import('#client').Source<V>) => void)} then_fn
* @param {null | ((anchor: Node, error: unknown) => void)} catch_fn
* @returns {void}
*/
export function await_block(anchor, get_input, pending_fn, then_fn, catch_fn) {
const component_context = current_component_context;
/** @type {any} */
let component_function;
if (DEV) {
component_function = component_context?.function ?? null;
}
var runes = is_runes();
var component_context = current_component_context;
/** @type {any} */
let input;
var component_function = DEV ? component_context?.function : null;
/** @type {V | Promise<V>} */
var input;
/** @type {import('#client').Effect | null} */
let pending_effect;
var pending_effect;
/** @type {import('#client').Effect | null} */
let then_effect;
var then_effect;
/** @type {import('#client').Effect | null} */
let catch_effect;
var catch_effect;
var input_source = runes
? source(/** @type {V} */ (undefined))
: mutable_source(/** @type {V} */ (undefined));
var error_source = runes ? source(undefined) : mutable_source(undefined);
var resolved = false;
/**
* @param {(anchor: Comment, value: any) => void} fn
* @param {any} value
* @param {PENDING | THEN | CATCH} state
* @param {boolean} restore
*/
function create_effect(fn, value) {
set_current_effect(effect);
set_current_reaction(effect); // TODO do we need both?
set_current_component_context(component_context);
if (DEV) {
set_dev_current_component_function(component_function);
function update(state, restore) {
resolved = true;
if (restore) {
set_current_effect(effect);
set_current_reaction(effect); // TODO do we need both?
set_current_component_context(component_context);
if (DEV) set_dev_current_component_function(component_function);
}
if (state === PENDING && pending_fn) {
if (pending_effect) resume_effect(pending_effect);
else pending_effect = branch(() => pending_fn(anchor));
}
if (state === THEN && then_fn) {
if (then_effect) resume_effect(then_effect);
else then_effect = branch(() => then_fn(anchor, input_source));
}
if (state === CATCH && catch_fn) {
if (catch_effect) resume_effect(catch_effect);
else catch_effect = branch(() => catch_fn(anchor, error_source));
}
if (state !== PENDING && pending_effect) {
pause_effect(pending_effect, () => (pending_effect = null));
}
var e = branch(() => fn(anchor, value));
if (DEV) {
set_dev_current_component_function(null);
if (state !== THEN && then_effect) {
pause_effect(then_effect, () => (then_effect = null));
}
if (state !== CATCH && catch_effect) {
pause_effect(catch_effect, () => (catch_effect = null));
}
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)
flush_sync();
if (restore) {
if (DEV) set_dev_current_component_function(null);
set_current_component_context(null);
set_current_reaction(null);
set_current_effect(null);
return e;
// without this, the DOM does not update until two ticks after the promise
// resolves, which is unexpected behaviour (and somewhat irksome to test)
flush_sync();
}
}
const effect = block(() => {
var effect = block(anchor, 0, () => {
if (input === (input = get_input())) return;
if (is_promise(input)) {
const promise = /** @type {Promise<any>} */ (input);
var promise = input;
if (pending_fn) {
if (pending_effect && (pending_effect.f & INERT) === 0) {
destroy_effect(pending_effect);
}
pending_effect = branch(() => pending_fn(anchor));
}
if (then_effect) pause_effect(then_effect);
if (catch_effect) pause_effect(catch_effect);
resolved = false;
promise.then(
(value) => {
if (promise !== input) return;
if (pending_effect) pause_effect(pending_effect);
if (then_fn) {
then_effect = create_effect(then_fn, value);
}
set(input_source, value);
update(THEN, true);
},
(error) => {
if (promise !== input) return;
if (pending_effect) pause_effect(pending_effect);
if (catch_fn) {
catch_effect = create_effect(catch_fn, error);
}
set(error_source, error);
update(CATCH, true);
}
);
} else {
if (pending_effect) pause_effect(pending_effect);
if (catch_effect) pause_effect(catch_effect);
if (then_fn) {
if (then_effect) {
destroy_effect(then_effect);
if (hydrating) {
if (pending_fn) {
pending_effect = branch(() => pending_fn(anchor));
}
then_effect = branch(() => then_fn(anchor, input));
} else {
// Wait a microtask before checking if we should show the pending state as
// the promise might have resolved by the next microtask.
queue_micro_task(() => {
if (!resolved) update(PENDING, true);
});
}
} else {
set(input_source, input);
update(THEN, false);
}
// Inert effects are proactively detached from the effect tree. Returning a noop

@ -24,12 +24,14 @@ import {
run_out_transitions,
pause_children,
pause_effect,
resume_effect
resume_effect,
get_first_node
} from '../../reactivity/effects.js';
import { source, mutable_source, set } from '../../reactivity/sources.js';
import { is_array, is_frozen } from '../../utils.js';
import { INERT, STATE_SYMBOL } from '../../constants.js';
import { queue_micro_task } from '../task.js';
import { current_effect } from '../../runtime.js';
/**
* The row of a keyed each block that is currently updating. We track this
@ -54,11 +56,12 @@ export function index(_, i) {
/**
* Pause multiple effects simultaneously, and coordinate their
* subsequent destruction. Used in each blocks
* @param {import('#client').EachState} state
* @param {import('#client').EachItem[]} items
* @param {null | Node} controlled_anchor
* @param {Map<any, import("#client").EachItem>} items_map
*/
function pause_effects(items, controlled_anchor, items_map) {
function pause_effects(state, items, controlled_anchor, items_map) {
/** @type {import('#client').TransitionManager[]} */
var transitions = [];
var length = items.length;
@ -77,7 +80,7 @@ function pause_effects(items, controlled_anchor, items_map) {
clear_text_content(parent_node);
parent_node.append(/** @type {Element} */ (controlled_anchor));
items_map.clear();
link(items[0].prev, items[length - 1].next);
link(state, items[0].prev, items[length - 1].next);
}
run_out_transitions(transitions, () => {
@ -85,8 +88,7 @@ function pause_effects(items, controlled_anchor, items_map) {
var item = items[i];
if (!is_controlled) {
items_map.delete(item.k);
item.o.remove();
link(item.prev, item.next);
link(state, item.prev, item.next);
}
destroy_effect(item.e, !is_controlled);
}
@ -105,7 +107,7 @@ function pause_effects(items, controlled_anchor, items_map) {
*/
export function each(anchor, flags, get_collection, get_key, render_fn, fallback_fn = null) {
/** @type {import('#client').EachState} */
var state = { flags, items: new Map(), next: null };
var state = { flags, items: new Map(), first: null };
var is_controlled = (flags & EACH_IS_CONTROLLED) !== 0;
@ -122,7 +124,7 @@ export function each(anchor, flags, get_collection, get_key, render_fn, fallback
/** @type {import('#client').Effect | null} */
var fallback = null;
block(() => {
block(anchor, 0, () => {
var collection = get_collection();
var array = is_array(collection)
@ -151,7 +153,7 @@ export function each(anchor, flags, get_collection, get_key, render_fn, fallback
if (hydrating) {
var is_else = /** @type {Comment} */ (anchor).data === HYDRATION_END_ELSE;
if (is_else !== (length === 0)) {
if (is_else !== (length === 0) || hydrate_start === undefined) {
// hydration mismatch — remove the server-rendered DOM and start over
remove(hydrate_nodes);
set_hydrating(false);
@ -164,8 +166,8 @@ export function each(anchor, flags, get_collection, get_key, render_fn, fallback
/** @type {Node} */
var child_anchor = hydrate_start;
/** @type {import('#client').EachItem | import('#client').EachState} */
var prev = state;
/** @type {import('#client').EachItem | null} */
var prev = null;
/** @type {import('#client').EachItem} */
var item;
@ -182,11 +184,10 @@ export function each(anchor, flags, get_collection, get_key, render_fn, fallback
break;
}
var child_open = /** @type {Comment} */ (child_anchor);
child_anchor = hydrate_anchor(child_anchor);
var value = array[i];
var key = get_key(value, i);
item = create_item(child_open, child_anchor, prev, null, value, key, i, render_fn, flags);
item = create_item(child_anchor, state, prev, null, value, key, i, render_fn, flags);
state.items.set(key, item);
child_anchor = /** @type {Comment} */ (child_anchor.nextSibling);
@ -244,14 +245,14 @@ function reconcile(array, state, anchor, render_fn, flags, get_key) {
var length = array.length;
var items = state.items;
var first = state.next;
var first = state.first;
var current = first;
/** @type {Set<import('#client').EachItem>} */
var seen = new Set();
/** @type {import('#client').EachState | import('#client').EachItem} */
var prev = state;
/** @type {import('#client').EachItem | null} */
var prev = null;
/** @type {Set<import('#client').EachItem>} */
var to_animate = new Set();
@ -293,16 +294,13 @@ function reconcile(array, state, anchor, render_fn, flags, get_key) {
item = items.get(key);
if (item === undefined) {
var child_open = empty();
var child_anchor = current ? current.o : anchor;
child_anchor.before(child_open);
var child_anchor = current ? get_first_node(current.e) : anchor;
prev = create_item(
child_open,
child_anchor,
state,
prev,
prev.next,
prev === null ? state.first : prev.next,
value,
key,
i,
@ -351,9 +349,9 @@ function reconcile(array, state, anchor, render_fn, flags, get_key) {
seen.delete(stashed[j]);
}
link(a.prev, b.next);
link(prev, a);
link(b, start);
link(state, a.prev, b.next);
link(state, prev, a);
link(state, b, start);
current = start;
prev = b;
@ -366,9 +364,9 @@ function reconcile(array, state, anchor, render_fn, flags, get_key) {
seen.delete(item);
move(item, current, anchor);
link(item.prev, item.next);
link(item, prev.next);
link(prev, item);
link(state, item.prev, item.next);
link(state, item, prev === null ? state.first : prev.next);
link(state, prev, item);
prev = item;
}
@ -418,7 +416,7 @@ function reconcile(array, state, anchor, render_fn, flags, get_key) {
}
}
pause_effects(to_destroy, controlled_anchor, items);
pause_effects(state, to_destroy, controlled_anchor, items);
}
if (is_animated) {
@ -428,6 +426,9 @@ function reconcile(array, state, anchor, render_fn, flags, get_key) {
}
});
}
/** @type {import('#client').Effect} */ (current_effect).first = state.first && state.first.e;
/** @type {import('#client').Effect} */ (current_effect).last = prev && prev.e;
}
/**
@ -451,9 +452,9 @@ function update_item(item, value, index, type) {
/**
* @template V
* @param {Comment | Text} open
* @param {Node} anchor
* @param {import('#client').EachItem | import('#client').EachState} prev
* @param {import('#client').EachState} state
* @param {import('#client').EachItem | null} prev
* @param {import('#client').EachItem | null} next
* @param {V} value
* @param {unknown} key
@ -462,7 +463,7 @@ function update_item(item, value, index, type) {
* @param {number} flags
* @returns {import('#client').EachItem}
*/
function create_item(open, anchor, prev, next, value, key, index, render_fn, flags) {
function create_item(anchor, state, prev, next, value, key, index, render_fn, flags) {
var previous_each_item = current_each_item;
try {
@ -480,16 +481,27 @@ function create_item(open, anchor, prev, next, value, key, index, render_fn, fla
a: null,
// @ts-expect-error
e: null,
o: open,
prev,
next
};
prev.next = item;
if (next !== null) next.prev = item;
current_each_item = item;
item.e = branch(() => render_fn(anchor, v, i));
item.e = branch(() => render_fn(anchor, v, i), hydrating);
item.e.prev = prev && prev.e;
item.e.next = next && next.e;
if (prev === null) {
state.first = item;
} else {
prev.next = item;
prev.e.next = item.e;
}
if (next !== null) {
next.prev = item;
next.e.prev = item.e;
}
return item;
} finally {
@ -503,10 +515,10 @@ function create_item(open, anchor, prev, next, value, key, index, render_fn, fla
* @param {Text | Element | Comment} anchor
*/
function move(item, next, anchor) {
var end = item.next ? item.next.o : anchor;
var dest = next ? next.o : anchor;
var end = item.next ? get_first_node(item.next.e) : anchor;
var dest = next ? get_first_node(next.e) : anchor;
var node = /** @type {import('#client').TemplateNode} */ (item.o);
var node = get_first_node(item.e);
while (node !== end) {
var next_node = /** @type {import('#client').TemplateNode} */ (node.nextSibling);
@ -516,11 +528,20 @@ function move(item, next, anchor) {
}
/**
*
* @param {import('#client').EachItem | import('#client').EachState} prev
* @param {import('#client').EachState} state
* @param {import('#client').EachItem | null} prev
* @param {import('#client').EachItem | null} next
*/
function link(prev, next) {
prev.next = next;
if (next !== null) next.prev = prev;
function link(state, prev, next) {
if (prev === null) {
state.first = next;
} else {
prev.next = next;
prev.e.next = next && next.e;
}
if (next !== null) {
next.prev = prev;
next.e.prev = prev && prev.e;
}
}

@ -1,30 +1,7 @@
import { derived } from '../../reactivity/deriveds.js';
import { render_effect } from '../../reactivity/effects.js';
import { current_effect, get } from '../../runtime.js';
import { is_array } from '../../utils.js';
import { hydrate_nodes, hydrating } from '../hydration.js';
import { create_fragment_from_html, remove } from '../reconciler.js';
import { push_template_node } from '../template.js';
/**
* @param {import('#client').Effect} effect
* @param {(Element | Comment | Text)[]} to_remove
* @returns {void}
*/
function remove_from_parent_effect(effect, to_remove) {
const dom = effect.dom;
if (is_array(dom)) {
for (let i = dom.length - 1; i >= 0; i--) {
if (to_remove.includes(dom[i])) {
dom.splice(i, 1);
break;
}
}
} else if (dom !== null && to_remove.includes(dom)) {
effect.dom = null;
}
}
import { block, branch, destroy_effect } from '../../reactivity/effects.js';
import { get_start, hydrate_nodes, hydrating } from '../hydration.js';
import { create_fragment_from_html } from '../reconciler.js';
import { assign_nodes } from '../template.js';
/**
* @param {Element | Text | Comment} anchor
@ -34,72 +11,52 @@ function remove_from_parent_effect(effect, to_remove) {
* @returns {void}
*/
export function html(anchor, get_value, svg, mathml) {
const parent_effect = anchor.parentNode !== current_effect?.dom ? current_effect : null;
let value = derived(get_value);
var value = '';
render_effect(() => {
var dom = html_to_dom(anchor, parent_effect, get(value), svg, mathml);
/** @type {import('#client').Effect | null} */
var effect;
if (dom) {
return () => {
if (parent_effect !== null) {
remove_from_parent_effect(parent_effect, is_array(dom) ? dom : [dom]);
}
remove(dom);
};
}
});
}
block(anchor, 0, () => {
if (value === (value = get_value())) return;
/**
* 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 {import('#client').Effect | null} effect
* @param {V} value
* @param {boolean} svg
* @param {boolean} mathml
* @returns {Element | Comment | (Element | Comment | Text)[]}
*/
function html_to_dom(target, effect, value, svg, mathml) {
if (hydrating) return hydrate_nodes;
var html = value + '';
if (svg) html = `<svg>${html}</svg>`;
else if (mathml) html = `<math>${html}</math>`;
if (effect) {
destroy_effect(effect);
effect = null;
}
// 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 (value === '') return;
if (svg || mathml) {
node = /** @type {Element} */ (node.firstChild);
}
effect = branch(() => {
if (hydrating) {
assign_nodes(get_start(), hydrate_nodes[hydrate_nodes.length - 1]);
return;
}
if (node.childNodes.length === 1) {
var child = /** @type {Text | Element | Comment} */ (node.firstChild);
target.before(child);
if (effect !== null) {
push_template_node(child, effect);
}
return child;
}
var html = value + '';
if (svg) html = `<svg>${html}</svg>`;
else if (mathml) html = `<math>${html}</math>`;
var nodes = /** @type {Array<Text | Element | Comment>} */ ([...node.childNodes]);
// Don't use create_fragment_with_script_from_html here because that would mean script tags are executed.
// @html is basically `.innerHTML = ...` and that doesn't execute scripts either due to security reasons.
/** @type {DocumentFragment | Element} */
var node = create_fragment_from_html(html);
if (svg || mathml) {
while (node.firstChild) {
target.before(node.firstChild);
}
} else {
target.before(node);
}
if (svg || mathml) {
node = /** @type {Element} */ (node.firstChild);
}
if (effect !== null) {
push_template_node(nodes, effect);
}
assign_nodes(
/** @type {import('#client').TemplateNode} */ (node.firstChild),
/** @type {import('#client').TemplateNode} */ (node.lastChild)
);
return nodes;
if (svg || mathml) {
while (node.firstChild) {
anchor.before(node.firstChild);
}
} else {
anchor.before(node);
}
});
});
}

@ -30,7 +30,7 @@ export function if_block(
var flags = elseif ? EFFECT_TRANSPARENT : 0;
block(() => {
block(anchor, flags, () => {
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 */
@ -78,5 +78,5 @@ export function if_block(
// continue in hydration mode
set_hydrating(true);
}
}, flags);
});
}

@ -16,7 +16,7 @@ export function key_block(anchor, get_key, render_fn) {
/** @type {import('#client').Effect} */
let effect;
block(() => {
block(anchor, 0, () => {
if (safe_not_equal(key, (key = get_key()))) {
if (effect) {
pause_effect(effect);

@ -2,26 +2,25 @@ import { add_snippet_symbol } from '../../../shared/validate.js';
import { EFFECT_TRANSPARENT } from '../../constants.js';
import { branch, block, destroy_effect } from '../../reactivity/effects.js';
import {
current_component_context,
dev_current_component_function,
set_dev_current_component_function
} from '../../runtime.js';
/**
* @template {(node: import('#client').TemplateNode, ...args: any[]) => import('#client').Dom} SnippetFn
* @param {import('#client').TemplateNode} anchor
* @param {() => SnippetFn | null | undefined} get_snippet
* @param {import('#client').TemplateNode} node
* @param {(() => any)[]} args
* @returns {void}
*/
export function snippet(get_snippet, node, ...args) {
export function snippet(anchor, get_snippet, ...args) {
/** @type {SnippetFn | null | undefined} */
var snippet;
/** @type {import('#client').Effect | null} */
var snippet_effect;
block(() => {
block(anchor, EFFECT_TRANSPARENT, () => {
if (snippet === (snippet = get_snippet())) return;
if (snippet_effect) {
@ -30,24 +29,22 @@ export function snippet(get_snippet, node, ...args) {
}
if (snippet) {
snippet_effect = branch(() => /** @type {SnippetFn} */ (snippet)(node, ...args));
snippet_effect = branch(() => /** @type {SnippetFn} */ (snippet)(anchor, ...args));
}
}, EFFECT_TRANSPARENT);
});
}
/**
* In development, wrap the snippet function so that it passes validation, and so that the
* correct component context is set for ownership checks
* @param {(node: import('#client').TemplateNode, ...args: any[]) => import('#client').Dom} fn
* @returns
* @param {any} component
*/
export function wrap_snippet(fn) {
let component = /** @type {import('#client').ComponentContext} */ (current_component_context);
export function wrap_snippet(fn, component) {
return add_snippet_symbol(
(/** @type {import('#client').TemplateNode} */ node, /** @type {any[]} */ ...args) => {
var previous_component_function = dev_current_component_function;
set_dev_current_component_function(component.function);
set_dev_current_component_function(component);
try {
return fn(node, ...args);

@ -1,22 +1,21 @@
import { block, branch, pause_effect } from '../../reactivity/effects.js';
// TODO seems weird that `anchor` is unused here — possible bug?
/**
* @template P
* @template {(props: P) => void} C
* @param {import('#client').TemplateNode} anchor
* @param {() => C} get_component
* @param {(component: C) => import('#client').Dom | void} render_fn
* @returns {void}
*/
export function component(get_component, render_fn) {
export function component(anchor, get_component, render_fn) {
/** @type {C} */
let component;
/** @type {import('#client').Effect | null} */
let effect;
block(() => {
block(anchor, 0, () => {
if (component === (component = get_component())) return;
if (effect) {

@ -12,31 +12,9 @@ import { set_should_intro } from '../../render.js';
import { current_each_item, set_current_each_item } from './each.js';
import { current_component_context, current_effect } from '../../runtime.js';
import { DEV } from 'esm-env';
import { is_array } from '../../utils.js';
import { push_template_node } from '../template.js';
import { assign_nodes } from '../template.js';
import { noop } from '../../../shared/utils.js';
/**
* @param {import('#client').Effect} effect
* @param {Element} from
* @param {Element} to
* @returns {void}
*/
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) {
dom[i] = to;
break;
}
}
} else if (dom === from) {
effect.dom = to;
}
}
/**
* @param {Comment | Element} node
* @param {() => string} get_tag
@ -47,7 +25,6 @@ function swap_block_dom(effect, from, to) {
* @returns {void}
*/
export function element(node, get_tag, is_svg, render_fn, get_namespace, location) {
const parent_effect = /** @type {import('#client').Effect} */ (current_effect);
const filename = DEV && location && current_component_context?.function.filename;
/** @type {string | null} */
@ -71,7 +48,7 @@ export function element(node, get_tag, is_svg, render_fn, get_namespace, locatio
*/
let each_item_block = current_each_item;
block(() => {
block(anchor, 0, () => {
const next_tag = get_tag() || null;
const ns = get_namespace
? get_namespace()
@ -113,6 +90,8 @@ export function element(node, get_tag, is_svg, render_fn, get_namespace, locatio
? document.createElementNS(ns, next_tag)
: document.createElement(next_tag);
assign_nodes(element, element);
if (DEV && location) {
// @ts-expect-error
element.__svelte_meta = {
@ -124,6 +103,10 @@ export function element(node, get_tag, is_svg, render_fn, get_namespace, locatio
};
}
if (prev_element && !hydrating) {
prev_element.remove();
}
if (render_fn) {
// 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
@ -144,15 +127,6 @@ export function element(node, get_tag, is_svg, render_fn, get_namespace, locatio
anchor.before(element);
if (!hydrating) {
if (prev_element) {
swap_block_dom(parent_effect, prev_element, element);
prev_element.remove();
} else {
push_template_node(element, parent_effect);
}
}
// See below
return noop;
});

@ -2,6 +2,7 @@ import { hydrate_anchor, hydrate_nodes, hydrating, set_hydrate_nodes } from '../
import { empty } from '../operations.js';
import { block } from '../../reactivity/effects.js';
import { HYDRATION_END, HYDRATION_START } from '../../../../constants.js';
import { HEAD_EFFECT } from '../../constants.js';
/**
* @type {Node | undefined}
@ -47,7 +48,7 @@ export function head(render_fn) {
}
try {
block(() => render_fn(anchor));
block(null, HEAD_EFFECT, () => render_fn(anchor));
} finally {
if (was_hydrating) {
set_hydrate_nodes(/** @type {import('#client').TemplateNode[]} */ (previous_hydrate_nodes));

@ -1,6 +1,6 @@
import { DEV } from 'esm-env';
import { hydrating } from '../hydration.js';
import { get_descriptors, get_prototype_of, map_get, map_set } from '../../utils.js';
import { get_descriptors, get_prototype_of } from '../../utils.js';
import {
AttributeAliases,
DelegatedEvents,
@ -16,29 +16,40 @@ import { queue_idle_task, queue_micro_task } from '../task.js';
/**
* The value/checked attribute in the template actually corresponds to the defaultValue property, so we need
* to remove it upon hydration to avoid a bug when someone resets the form value.
* @param {HTMLInputElement} dom
* @param {HTMLInputElement} input
* @returns {void}
*/
export function remove_input_attr_defaults(dom) {
if (hydrating) {
let already_removed = false;
// We try and remove the default attributes later, rather than sync during hydration.
// Doing it sync during hydration has a negative impact on performance, but deferring the
// work in an idle task alleviates this greatly. If a form reset event comes in before
// the idle callback, then we ensure the input defaults are cleared just before.
const remove_defaults = () => {
if (already_removed) return;
already_removed = true;
const value = dom.getAttribute('value');
set_attribute(dom, 'value', null);
set_attribute(dom, 'checked', null);
if (value) dom.value = value;
};
// @ts-expect-error
dom.__on_r = remove_defaults;
queue_idle_task(remove_defaults);
add_form_reset_listener();
}
export function remove_input_defaults(input) {
if (!hydrating) return;
var already_removed = false;
// We try and remove the default attributes later, rather than sync during hydration.
// Doing it sync during hydration has a negative impact on performance, but deferring the
// work in an idle task alleviates this greatly. If a form reset event comes in before
// the idle callback, then we ensure the input defaults are cleared just before.
var remove_defaults = () => {
if (already_removed) return;
already_removed = true;
// Remove the attributes but preserve the values
if (input.hasAttribute('value')) {
var value = input.value;
set_attribute(input, 'value', null);
input.value = value;
}
if (input.hasAttribute('checked')) {
var checked = input.checked;
set_attribute(input, 'checked', null);
input.checked = checked;
}
};
// @ts-expect-error
input.__on_r = remove_defaults;
queue_idle_task(remove_defaults);
add_form_reset_listener();
}
/**
@ -155,8 +166,8 @@ export function set_attributes(element, prev, next, lowercase_attributes, css_ha
next.class = '';
}
var setters = map_get(setters_cache, element.nodeName);
if (!setters) map_set(setters_cache, element.nodeName, (setters = get_setters(element)));
var setters = setters_cache.get(element.nodeName);
if (!setters) setters_cache.set(element.nodeName, (setters = get_setters(element)));
// @ts-expect-error
var attributes = /** @type {Record<string, unknown>} **/ (element.__attributes ??= {});

@ -22,7 +22,8 @@ function time_ranges_to_array(ranges) {
export function bind_current_time(media, get_value, update) {
/** @type {number} */
var raf_id;
var updating = false;
/** @type {number} */
var value;
// Ideally, listening to timeupdate would be enough, but it fires too infrequently for the currentTime
// binding, which is why we use a raf loop, too. We additionally still listen to timeupdate because
@ -34,22 +35,21 @@ export function bind_current_time(media, get_value, update) {
raf_id = requestAnimationFrame(callback);
}
updating = true;
update(media.currentTime);
var next_value = media.currentTime;
if (value !== next_value) {
update((value = next_value));
}
};
raf_id = requestAnimationFrame(callback);
media.addEventListener('timeupdate', callback);
render_effect(() => {
var value = get_value();
var next_value = Number(get_value());
// through isNaN we also allow number strings, which is more robust
if (!updating && !isNaN(/** @type {any} */ (value))) {
media.currentTime = /** @type {number} */ (value);
if (value !== next_value && !isNaN(/** @type {any} */ (next_value))) {
media.currentTime = value = next_value;
}
updating = false;
});
teardown(() => cancelAnimationFrame(raf_id));
@ -113,22 +113,21 @@ export function bind_ready_state(media, update) {
* @param {(playback_rate: number) => void} update
*/
export function bind_playback_rate(media, get_value, update) {
var 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.
// Needs to happen after element is inserted into the dom (which is guaranteed by using effect),
// else playback will be set back to 1 by the browser
effect(() => {
var value = get_value();
var value = Number(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);
if (value !== media.playbackRate && !isNaN(value)) {
media.playbackRate = value;
}
});
// Start listening to ratechange events after the element is inserted into the dom,
// else playback will be set to 1 by the browser
effect(() => {
listen(media, ['ratechange'], () => {
if (!updating) update(media.playbackRate);
updating = false;
update(media.playbackRate);
});
});
}
@ -200,9 +199,7 @@ export function bind_paused(media, get_value, update) {
* @param {(volume: number) => void} update
*/
export function bind_volume(media, get_value, update) {
var updating = false;
var callback = () => {
updating = true;
update(media.volume);
};
@ -213,14 +210,11 @@ export function bind_volume(media, get_value, update) {
listen(media, ['volumechange'], callback, false);
render_effect(() => {
var value = get_value();
var value = Number(get_value());
// through isNaN we also allow number strings, which is more robust
if (!updating && !isNaN(/** @type {any} */ (value))) {
media.volume = /** @type {number} */ (value);
if (value !== media.volume && !isNaN(value)) {
media.volume = value;
}
updating = false;
});
}
@ -230,10 +224,7 @@ export function bind_volume(media, get_value, update) {
* @param {(muted: boolean) => void} update
*/
export function bind_muted(media, get_value, update) {
var updating = false;
var callback = () => {
updating = true;
update(media.muted);
};
@ -244,9 +235,8 @@ export function bind_muted(media, get_value, update) {
listen(media, ['volumechange'], callback, false);
render_effect(() => {
var value = get_value();
var value = !!get_value();
if (!updating) media.muted = !!value;
updating = false;
if (media.muted !== value) media.muted = value;
});
}

@ -39,10 +39,12 @@ export function select_option(select, value, mounting) {
* @param {() => V} [get_value]
*/
export function init_select(select, get_value) {
let mounting = true;
effect(() => {
if (get_value) {
select_option(select, untrack(get_value));
select_option(select, untrack(get_value), mounting);
}
mounting = false;
var observer = new MutationObserver(() => {
// @ts-ignore

@ -1,4 +1,5 @@
import { effect, teardown } from '../../../reactivity/effects.js';
import { untrack } from '../../../runtime.js';
/**
* Resize observer singleton.
@ -100,7 +101,8 @@ export function bind_element_size(element, type, update) {
var unsub = resize_observer_border_box.observe(element, () => update(element[type]));
effect(() => {
update(element[type]);
// The update could contain reads which should be ignored
untrack(() => update(element[type]));
return unsub;
});
}

@ -18,7 +18,7 @@ export function bind_content_editable(property, element, get_value, update) {
var value = get_value();
if (element[property] !== value) {
if (value === null) {
if (value == null) {
// @ts-ignore
var non_null_value = element[property];
update(non_null_value);

@ -44,7 +44,7 @@ export function create_event(event_name, dom, handler, options) {
function target_handler(/** @type {Event} */ event) {
if (!options.capture) {
// Only call in the bubble phase, else delegated events would be called before the capturing events
handle_event_propagation(dom, event);
handle_event_propagation.call(dom, event);
}
if (!event.cancelBubble) {
return handler.call(this, event);
@ -143,11 +143,12 @@ export function delegate(events) {
}
/**
* @param {EventTarget} handler_element
* @this {EventTarget}
* @param {Event} event
* @returns {void}
*/
export function handle_event_propagation(handler_element, event) {
export function handle_event_propagation(event) {
var handler_element = this;
var owner_document = /** @type {Node} */ (handler_element).ownerDocument;
var event_name = event.type;
var path = event.composedPath?.() || [];

@ -30,6 +30,17 @@ export function set_hydrate_nodes(nodes) {
hydrate_start = nodes && nodes[0];
}
/**
* When assigning nodes to an effect during hydration, we typically want the hydration boundary comment node
* immediately before `hydrate_start`. In some cases, this comment doesn't exist because we optimized it away.
* TODO it might be worth storing this value separately rather than retrieving it with `previousSibling`
*/
export function get_start() {
return /** @type {import('#client').TemplateNode} */ (
hydrate_start.previousSibling ?? hydrate_start
);
}
/**
* 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`

@ -85,13 +85,13 @@ export function first_child(fragment, is_text) {
// text node to hydrate — we must therefore create one
if (is_text && hydrate_start?.nodeType !== 3) {
var text = empty();
var dom = /** @type {import('#client').TemplateNode[]} */ (
/** @type {import('#client').Effect} */ (current_effect).dom
);
var effect = /** @type {import('#client').Effect} */ (current_effect);
dom.unshift(text);
hydrate_start?.before(text);
if (effect.nodes?.start === hydrate_start) {
effect.nodes.start = text;
}
hydrate_start?.before(text);
return text;
}
@ -122,13 +122,7 @@ export function sibling(node, is_text = false) {
// text node to hydrate — we must therefore create one
if (is_text && type !== 3) {
var text = empty();
var dom = /** @type {import('#client').TemplateNode[]} */ (
/** @type {import('#client').Effect} */ (current_effect).dom
);
dom.unshift(text);
next_sibling?.before(text);
return text;
}

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

Loading…
Cancel
Save