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

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

@ -1,5 +1,6 @@
{ {
"search.exclude": { "search.exclude": {
"sites/svelte-5-preview/static/*": true "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, "private": true,
"type": "module", "type": "module",
"license": "MIT", "license": "MIT",
"packageManager": "pnpm@9.2.0", "packageManager": "pnpm@9.4.0",
"engines": { "engines": {
"pnpm": "^9.0.0" "pnpm": "^9.0.0"
}, },
@ -23,29 +23,31 @@
"test": "vitest run", "test": "vitest run",
"test-output": "vitest run --coverage --reporter=json --outputFile=sites/svelte-5-preview/src/routes/status/results.json", "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: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": { "devDependencies": {
"@changesets/cli": "^2.27.1", "@changesets/cli": "^2.27.6",
"@sveltejs/eslint-config": "^7.0.1", "@sveltejs/eslint-config": "^8.0.1",
"@svitejs/changesets-changelog-github-compact": "^1.1.0", "@svitejs/changesets-changelog-github-compact": "^1.1.0",
"@types/node": "^20.11.5", "@types/node": "^20.11.5",
"@vitest/coverage-v8": "^1.2.1", "@vitest/coverage-v8": "^1.2.1",
"concurrently": "^8.2.2", "eslint": "^9.6.0",
"cross-env": "^7.0.3",
"eslint": "^9.0.0",
"eslint-plugin-lube": "^0.4.3", "eslint-plugin-lube": "^0.4.3",
"jsdom": "22.0.0", "jsdom": "22.0.0",
"playwright": "^1.41.1", "playwright": "^1.41.1",
"prettier": "^3.2.4", "prettier": "^3.2.4",
"prettier-plugin-svelte": "^3.1.2", "prettier-plugin-svelte": "^3.1.2",
"typescript": "^5.3.3", "typescript": "^5.5.2",
"typescript-eslint": "^8.0.0-alpha.20", "typescript-eslint": "^8.0.0-alpha.34",
"v8-natives": "^1.2.5",
"vitest": "^1.2.1" "vitest": "^1.2.1"
}, },
"pnpm": { "pnpm": {
"overrides": { "overrides": {
"jimp>xml2js": "^0.6.0" "xml2js": "^0.6.0"
} }
} }
} }

@ -1,5 +1,103 @@
# svelte # 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 ## 5.0.0-next.158
### Patch Changes ### Patch Changes

@ -113,6 +113,7 @@ export interface DOMAttributes<T extends EventTarget> {
'on:beforeinput'?: EventHandler<InputEvent, T> | undefined | null; 'on:beforeinput'?: EventHandler<InputEvent, T> | undefined | null;
onbeforeinput?: EventHandler<InputEvent, T> | undefined | null; onbeforeinput?: EventHandler<InputEvent, T> | undefined | null;
onbeforeinputcapture?: 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; 'on:input'?: FormEventHandler<T> | undefined | null;
oninput?: FormEventHandler<T> | undefined | null; oninput?: FormEventHandler<T> | undefined | null;
oninputcapture?: 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 > 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 ## legacy_export_invalid
> Cannot use `export let` in runes mode — use `$props()` instead > Cannot use `export let` in runes mode — use `$props()` instead
@ -74,6 +78,10 @@
> Cannot use `$props()` more than once > 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_invalid_identifier
> `$props()` can only be used with an object destructuring pattern > `$props()` can only be used with an object destructuring pattern

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

@ -25,12 +25,12 @@ await createBundle({
[pkg.name]: `${dir}/src/index.d.ts`, [pkg.name]: `${dir}/src/index.d.ts`,
[`${pkg.name}/action`]: `${dir}/src/action/public.d.ts`, [`${pkg.name}/action`]: `${dir}/src/action/public.d.ts`,
[`${pkg.name}/animate`]: `${dir}/src/animate/public.d.ts`, [`${pkg.name}/animate`]: `${dir}/src/animate/public.d.ts`,
[`${pkg.name}/compiler`]: `${dir}/src/compiler/index.js`, [`${pkg.name}/compiler`]: `${dir}/src/compiler/public.d.ts`,
[`${pkg.name}/easing`]: `${dir}/src/easing/index.js`, [`${pkg.name}/easing`]: `${dir}/src/easing/index.js`,
[`${pkg.name}/legacy`]: `${dir}/src/legacy/legacy-client.js`, [`${pkg.name}/legacy`]: `${dir}/src/legacy/legacy-client.js`,
[`${pkg.name}/motion`]: `${dir}/src/motion/public.d.ts`, [`${pkg.name}/motion`]: `${dir}/src/motion/public.d.ts`,
[`${pkg.name}/reactivity`]: `${dir}/src/reactivity/index-client.js`, [`${pkg.name}/reactivity`]: `${dir}/src/reactivity/index-client.js`,
[`${pkg.name}/server`]: `${dir}/src/server/index.js`, [`${pkg.name}/server`]: `${dir}/src/server/index.d.ts`,
[`${pkg.name}/store`]: `${dir}/src/store/public.d.ts`, [`${pkg.name}/store`]: `${dir}/src/store/public.d.ts`,
[`${pkg.name}/transition`]: `${dir}/src/transition/public.d.ts`, [`${pkg.name}/transition`]: `${dir}/src/transition/public.d.ts`,
[`${pkg.name}/events`]: `${dir}/src/events/index.js`, [`${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 * 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: * 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"); 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 * Cannot use `export let` in runes mode use `$props()` instead
* @param {null | number | NodeLike} node * @param {null | number | NodeLike} node
@ -267,6 +276,15 @@ export function props_duplicate(node) {
e(node, "props_duplicate", "Cannot use `$props()` more than once"); 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 * `$props()` can only be used with an object destructuring pattern
* @param {null | number | NodeLike} node * @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 { walk } from 'zimmerframe';
import { import {
regex_ends_with_whitespaces, 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. * 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) { function remove_surrounding_whitespace_nodes(nodes) {
const first = nodes.at(0); 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 * Transform our nice modern AST into the monstrosity emitted by Svelte 4
* @param {string} source * @param {string} source
* @param {import('#compiler').Root} ast * @param {Root} ast
* @returns {import('./types/legacy-nodes.js').LegacyRoot} * @returns {Legacy.LegacyRoot}
*/ */
export function convert(source, ast) { export function convert(source, ast) {
const root = const root = /** @type {SvelteNode | Legacy.LegacySvelteNode} */ (ast);
/** @type {import('./types/template.js').SvelteNode | import('./types/legacy-nodes.js').LegacySvelteNode} */ (
ast
);
return /** @type {import('./types/legacy-nodes.js').LegacyRoot} */ ( return /** @type {Legacy.LegacyRoot} */ (
walk(root, null, { walk(root, null, {
_(node, { next }) { _(node, { next }) {
// @ts-ignore // @ts-ignore
@ -74,8 +74,8 @@ export function convert(source, ast) {
let end = null; let end = null;
if (node.fragment.nodes.length > 0) { if (node.fragment.nodes.length > 0) {
const first = /** @type {import('#compiler').BaseNode} */ (node.fragment.nodes.at(0)); const first = /** @type {BaseNode} */ (node.fragment.nodes.at(0));
const last = /** @type {import('#compiler').BaseNode} */ (node.fragment.nodes.at(-1)); const last = /** @type {BaseNode} */ (node.fragment.nodes.at(-1));
start = first.start; start = first.start;
end = last.end; end = last.end;
@ -229,25 +229,20 @@ export function convert(source, ast) {
end: node.end, end: node.end,
name: node.name, name: node.name,
attributes: node.attributes.map( attributes: node.attributes.map(
(child) => (child) => /** @type {Legacy.LegacyAttributeLike} */ (visit(child))
/** @type {import('./types/legacy-nodes.js').LegacyAttributeLike} */ (visit(child))
), ),
children: node.fragment.nodes.map( children: node.fragment.nodes.map(
(child) => (child) => /** @type {Legacy.LegacyElementLike} */ (visit(child))
/** @type {import('./types/legacy-nodes.js').LegacyElementLike} */ (visit(child))
) )
}; };
}, },
// @ts-ignore // @ts-ignore
ConstTag(node) { ConstTag(node) {
if ( if (/** @type {Legacy.LegacyConstTag} */ (node).expression !== undefined) {
/** @type {import('./types/legacy-nodes.js').LegacyConstTag} */ (node).expression !==
undefined
) {
return node; return node;
} }
const modern_node = /** @type {import('#compiler').ConstTag} */ (node); const modern_node = /** @type {ConstTag} */ (node);
const { id: left } = { ...modern_node.declaration.declarations[0] }; const { id: left } = { ...modern_node.declaration.declarations[0] };
// @ts-ignore // @ts-ignore
delete left.typeAnnotation; delete left.typeAnnotation;
@ -274,8 +269,7 @@ export function convert(source, ast) {
end: node.end, end: node.end,
expression: node.expression, expression: node.expression,
children: node.fragment.nodes.map( children: node.fragment.nodes.map(
(child) => (child) => /** @type {Legacy.LegacyElementLike} */ (visit(child))
/** @type {import('./types/legacy-nodes.js').LegacyElementLike} */ (visit(child))
) )
}; };
}, },
@ -354,8 +348,7 @@ export function convert(source, ast) {
start, start,
end: end, end: end,
children: node.alternate.nodes.map( children: node.alternate.nodes.map(
(child) => (child) => /** @type {Legacy.LegacyElementLike} */ (visit(child))
/** @type {import('./types/legacy-nodes.js').LegacyElementLike} */ (visit(child))
) )
}; };
} }
@ -368,8 +361,7 @@ export function convert(source, ast) {
end: node.end, end: node.end,
expression: node.test, expression: node.test,
children: node.consequent.nodes.map( children: node.consequent.nodes.map(
(child) => (child) => /** @type {Legacy.LegacyElementLike} */ (visit(child))
/** @type {import('./types/legacy-nodes.js').LegacyElementLike} */ (visit(child))
), ),
else: elseblock, else: elseblock,
elseif: node.elseif ? true : undefined elseif: node.elseif ? true : undefined
@ -407,12 +399,10 @@ export function convert(source, ast) {
end: node.end, end: node.end,
name: node.name, name: node.name,
attributes: node.attributes.map( attributes: node.attributes.map(
(child) => (child) => /** @type {Legacy.LegacyAttributeLike} */ (visit(child))
/** @type {import('./types/legacy-nodes.js').LegacyAttributeLike} */ (visit(child))
), ),
children: node.fragment.nodes.map( children: node.fragment.nodes.map(
(child) => (child) => /** @type {Legacy.LegacyElementLike} */ (visit(child))
/** @type {import('./types/legacy-nodes.js').LegacyElementLike} */ (visit(child))
) )
}; };
}, },
@ -433,12 +423,10 @@ export function convert(source, ast) {
start: node.start, start: node.start,
end: node.end, end: node.end,
attributes: node.attributes.map( attributes: node.attributes.map(
(child) => (child) => /** @type {Legacy.LegacyAttributeLike} */ (visit(child))
/** @type {import('./types/legacy-nodes.js').LegacyAttributeLike} */ (visit(child))
), ),
children: node.fragment.nodes.map( children: node.fragment.nodes.map(
(child) => (child) => /** @type {Legacy.LegacyElementLike} */ (visit(child))
/** @type {import('./types/legacy-nodes.js').LegacyElementLike} */ (visit(child))
) )
}; };
}, },
@ -450,12 +438,10 @@ export function convert(source, ast) {
end: node.end, end: node.end,
expression: node.expression, expression: node.expression,
attributes: node.attributes.map( attributes: node.attributes.map(
(child) => (child) => /** @type {Legacy.LegacyAttributeLike} */ (visit(child))
/** @type {import('./types/legacy-nodes.js').LegacyAttributeLike} */ (visit(child))
), ),
children: node.fragment.nodes.map( children: node.fragment.nodes.map(
(child) => (child) => /** @type {Legacy.LegacyElementLike} */ (visit(child))
/** @type {import('./types/legacy-nodes.js').LegacyElementLike} */ (visit(child))
) )
}; };
}, },
@ -466,17 +452,15 @@ export function convert(source, ast) {
start: node.start, start: node.start,
end: node.end, end: node.end,
attributes: node.attributes.map( attributes: node.attributes.map(
(child) => (child) => /** @type {Legacy.LegacyAttributeLike} */ (visit(child))
/** @type {import('./types/legacy-nodes.js').LegacyAttributeLike} */ (visit(child))
), ),
children: node.fragment.nodes.map( children: node.fragment.nodes.map(
(child) => (child) => /** @type {Legacy.LegacyElementLike} */ (visit(child))
/** @type {import('./types/legacy-nodes.js').LegacyElementLike} */ (visit(child))
) )
}; };
}, },
SvelteElement(node, { visit }) { SvelteElement(node, { visit }) {
/** @type {import('estree').Expression | string} */ /** @type {Expression | string} */
let tag = node.tag; let tag = node.tag;
if ( if (
tag.type === 'Literal' && tag.type === 'Literal' &&
@ -503,11 +487,10 @@ export function convert(source, ast) {
start: node.start, start: node.start,
end: node.end, end: node.end,
attributes: node.attributes.map( 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( children: node.fragment.nodes.map(
(child) => (child) => /** @type {Legacy.LegacyElementLike} */ (visit(child))
/** @type {import('./types/legacy-nodes.js').LegacyElementLike} */ (visit(child))
) )
}; };
}, },
@ -518,12 +501,10 @@ export function convert(source, ast) {
start: node.start, start: node.start,
end: node.end, end: node.end,
attributes: node.attributes.map( attributes: node.attributes.map(
(child) => (child) => /** @type {Legacy.LegacyAttributeLike} */ (visit(child))
/** @type {import('./types/legacy-nodes.js').LegacyAttributeLike} */ (visit(child))
), ),
children: node.fragment.nodes.map( children: node.fragment.nodes.map(
(child) => (child) => /** @type {Legacy.LegacyElementLike} */ (visit(child))
/** @type {import('./types/legacy-nodes.js').LegacyElementLike} */ (visit(child))
) )
}; };
}, },
@ -534,8 +515,7 @@ export function convert(source, ast) {
start: node.start, start: node.start,
end: node.end, end: node.end,
attributes: node.attributes.map( attributes: node.attributes.map(
(child) => (child) => /** @type {Legacy.LegacyAttributeLike} */ (visit(child))
/** @type {import('./types/legacy-nodes.js').LegacyAttributeLike} */ (visit(child))
) )
}; };
}, },
@ -546,12 +526,10 @@ export function convert(source, ast) {
start: node.start, start: node.start,
end: node.end, end: node.end,
attributes: node.attributes.map( attributes: node.attributes.map(
(child) => (child) => /** @type {Legacy.LegacyAttributeLike} */ (visit(child))
/** @type {import('./types/legacy-nodes.js').LegacyAttributeLike} */ (visit(child))
), ),
children: node.fragment.nodes.map( children: node.fragment.nodes.map(
(child) => (child) => /** @type {Legacy.LegacyElementLike} */ (visit(child))
/** @type {import('./types/legacy-nodes.js').LegacyElementLike} */ (visit(child))
) )
}; };
}, },
@ -562,12 +540,10 @@ export function convert(source, ast) {
start: node.start, start: node.start,
end: node.end, end: node.end,
attributes: node.attributes.map( attributes: node.attributes.map(
(child) => (child) => /** @type {Legacy.LegacyAttributeLike} */ (visit(child))
/** @type {import('./types/legacy-nodes.js').LegacyAttributeLike} */ (visit(child))
), ),
children: node.fragment.nodes.map( children: node.fragment.nodes.map(
(child) => (child) => /** @type {Legacy.LegacyElementLike} */ (visit(child))
/** @type {import('./types/legacy-nodes.js').LegacyElementLike} */ (visit(child))
) )
}; };
}, },
@ -575,7 +551,7 @@ export function convert(source, ast) {
const parent = path.at(-1); const parent = path.at(-1);
if (parent?.type === 'RegularElement' && parent.name === 'style') { if (parent?.type === 'RegularElement' && parent.name === 'style') {
// these text nodes are missing `raw` for some dumb reason // these text nodes are missing `raw` for some dumb reason
return /** @type {import('./types/template.js').Text} */ ({ return /** @type {Text} */ ({
type: 'Text', type: 'Text',
start: node.start, start: node.start,
end: node.end, end: node.end,
@ -590,12 +566,10 @@ export function convert(source, ast) {
start: node.start, start: node.start,
end: node.end, end: node.end,
attributes: node.attributes.map( attributes: node.attributes.map(
(child) => (child) => /** @type {Legacy.LegacyAttributeLike} */ (visit(child))
/** @type {import('./types/legacy-nodes.js').LegacyAttributeLike} */ (visit(child))
), ),
children: node.fragment.nodes.map( children: node.fragment.nodes.map(
(child) => (child) => /** @type {Legacy.LegacyElementLike} */ (visit(child))
/** @type {import('./types/legacy-nodes.js').LegacyElementLike} */ (visit(child))
) )
}; };
}, },

@ -322,7 +322,10 @@ const instance_script = {
state.str.prependLeft(/** @type {number} */ (declarator.init.start), '$state('); state.str.prependLeft(/** @type {number} */ (declarator.init.start), '$state(');
state.str.appendRight(/** @type {number} */ (declarator.init.end), ')'); state.str.appendRight(/** @type {number} */ (declarator.init.end), ')');
} else { } 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 * @param {State} state
*/ */
function handle_events(node, state) { function handle_events(element, state) {
/** @type {Map<string, import('#compiler').OnDirective[]>} */ /** @type {Map<string, import('#compiler').OnDirective[]>} */
const handlers = new Map(); const handlers = new Map();
for (const attribute of node.attributes) { for (const attribute of element.attributes) {
if (attribute.type !== 'OnDirective') continue; if (attribute.type !== 'OnDirective') continue;
let name = `on${attribute.name}`; let name = `on${attribute.name}`;
@ -625,6 +628,7 @@ function handle_events(node, state) {
for (let i = 0; i < nodes.length - 1; i += 1) { for (let i = 0; i < nodes.length - 1; i += 1) {
const node = nodes[i]; const node = nodes[i];
const indent = get_indent(state, node, element);
if (node.expression) { if (node.expression) {
let body = ''; let body = '';
if (node.expression.type === 'ArrowFunctionExpression') { if (node.expression.type === 'ArrowFunctionExpression') {
@ -638,19 +642,20 @@ function handle_events(node, state) {
/** @type {number} */ (node.expression.end) /** @type {number} */ (node.expression.end)
)}();`; )}();`;
} }
// TODO check how many indents needed
for (const modifier of node.modifiers) { for (const modifier of node.modifiers) {
if (modifier === 'stopPropagation') { if (modifier === 'stopPropagation') {
body = `\n${state.indent}${payload_name}.stopPropagation();\n${body}`; body = `\n${indent}${payload_name}.stopPropagation();\n${body}`;
} else if (modifier === 'preventDefault') { } 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') { } else if (modifier === 'stopImmediatePropagation') {
body = `\n${state.indent}${payload_name}.stopImmediatePropagation();\n${body}`; body = `\n${indent}${payload_name}.stopImmediatePropagation();\n${body}`;
} else { } 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 { } else {
if (!local) { if (!local) {
local = state.scope.generate(`on${node.name}`); local = state.scope.generate(`on${node.name}`);
@ -663,7 +668,7 @@ function handle_events(node, state) {
type: '(event: any) => void' 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); 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'); state.str.appendRight(last.start + last.name.length + 3, 'capture');
} }
const indent = get_indent(state, last, element);
for (const modifier of last.modifiers) { for (const modifier of last.modifiers) {
if (modifier === 'stopPropagation') { if (modifier === 'stopPropagation') {
prepend += `\n${state.indent}${payload_name}.stopPropagation();\n`; prepend += `\n${indent}${payload_name}.stopPropagation();\n`;
} else if (modifier === 'preventDefault') { } else if (modifier === 'preventDefault') {
prepend += `\n${state.indent}${payload_name}.preventDefault();\n`; prepend += `\n${indent}${payload_name}.preventDefault();\n`;
} else if (modifier === 'stopImmediatePropagation') { } else if (modifier === 'stopImmediatePropagation') {
prepend += `\n${state.indent}${payload_name}.stopImmediatePropagation();\n`; prepend += `\n${indent}${payload_name}.stopImmediatePropagation();\n`;
} else if (modifier !== 'capture') { } 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); pos = /** @type {number} */ (pos) + (needs_curlies ? 0 : 1);
if (needs_curlies && state.str.original[pos - 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 // 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(pos - 1, pos, `{${prepend}${indent}`);
state.str.update(end, end + 1, '\n}'); state.str.update(end, end + 1, `\n${indent.slice(state.indent.length)}}`);
} else { } else {
state.str.prependRight(pos, `${needs_curlies ? '{' : ''}${prepend}${state.indent}`); state.str.prependRight(pos, `${needs_curlies ? '{' : ''}${prepend}${indent}`);
state.str.appendRight(end, `\n${needs_curlies ? '}' : ''}`); state.str.appendRight(
end,
`\n${indent.slice(state.indent.length)}${needs_curlies ? '}' : ''}`
);
} }
} else { } else {
state.str.update( state.str.update(
/** @type {number} */ (last.expression.start), /** @type {number} */ (last.expression.start),
/** @type {number} */ (last.expression.end), /** @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.start),
/** @type {number} */ (last.expression.end) /** @type {number} */ (last.expression.end)
)}?.(${payload_name});\n}` )}?.(${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 {import('#compiler').OnDirective} last
* @param {State} state * @param {State} state

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

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

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

@ -1086,6 +1086,8 @@ function check_element(node, state) {
if ( if (
node.type === 'SvelteElement' || node.type === 'SvelteElement' ||
node.type === 'SlotElement' || node.type === 'SlotElement' ||
node.type === 'Component' ||
node.type === 'RenderTag' ||
(node.type === 'RegularElement' && (node.type === 'RegularElement' &&
(a11y_labelable.includes(node.name) || node.name === 'slot')) (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 { warn_unused } from './css/css-warn.js';
import { extract_svelte_ignore } from '../../utils/extract_svelte_ignore.js'; import { extract_svelte_ignore } from '../../utils/extract_svelte_ignore.js';
import { ignore_map, ignore_stack, pop_ignore, push_ignore } from '../../state.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 * @param {import('#compiler').Script | null} script
@ -111,23 +112,24 @@ function get_delegated_event(event_name, handler, context) {
if (binding != null) { if (binding != null) {
for (const { path } of binding.references) { for (const { path } of binding.references) {
const parent = path.at(-1); const parent = path.at(-1);
if (parent == null) { if (parent == null) return non_hoistable;
return non_hoistable;
} const grandparent = path.at(-2);
/** @type {import('#compiler').RegularElement | null} */ /** @type {import('#compiler').RegularElement | null} */
let element = null; let element = null;
/** @type {string | null} */ /** @type {string | null} */
let event_name = null; let event_name = null;
if (parent.type === 'OnDirective') { if (parent.type === 'OnDirective') {
element = /** @type {import('#compiler').RegularElement} */ (path.at(-2)); element = /** @type {import('#compiler').RegularElement} */ (grandparent);
event_name = parent.name; event_name = parent.name;
} else if ( } else if (
parent.type === 'ExpressionTag' && 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)); 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); event_name = get_attribute_event_name(attribute.name);
} }
@ -968,34 +970,42 @@ const runes_scope_tweaker = {
if (rune === '$props') { if (rune === '$props') {
state.analysis.needs_props = true; state.analysis.needs_props = true;
for (const property of /** @type {import('estree').ObjectPattern} */ (node.id).properties) { if (node.id.type === 'Identifier') {
if (property.type !== 'Property') continue; const binding = /** @type {import('#compiler').Binding} */ (state.scope.get(node.id.name));
binding.initial = null; // else would be $props()
const name = binding.kind = 'rest_prop';
property.value.type === 'AssignmentPattern' } else {
? /** @type {import('estree').Identifier} */ (property.value.left).name equal(node.id.type, 'ObjectPattern');
: /** @type {import('estree').Identifier} */ (property.value).name;
const alias = for (const property of node.id.properties) {
property.key.type === 'Identifier' if (property.type !== 'Property') continue;
? property.key.name
: String(/** @type {import('estree').Literal} */ (property.key).value); const name =
let initial = property.value.type === 'AssignmentPattern' ? property.value.right : null; property.value.type === 'AssignmentPattern'
? /** @type {import('estree').Identifier} */ (property.value.left).name
const binding = /** @type {import('#compiler').Binding} */ (state.scope.get(name)); : /** @type {import('estree').Identifier} */ (property.value).name;
binding.prop_alias = alias; const alias =
property.key.type === 'Identifier'
// rewire initial from $props() to the actual initial value, stripping $bindable() if necessary ? property.key.name
if ( : String(/** @type {import('estree').Literal} */ (property.key).value);
initial?.type === 'CallExpression' && let initial = property.value.type === 'AssignmentPattern' ? property.value.right : null;
initial.callee.type === 'Identifier' &&
initial.callee.name === '$bindable' const binding = /** @type {import('#compiler').Binding} */ (state.scope.get(name));
) { binding.prop_alias = alias;
binding.initial = /** @type {import('estree').Expression | null} */ (
initial.arguments[0] ?? null // rewire initial from $props() to the actual initial value, stripping $bindable() if necessary
); if (
binding.kind = 'bindable_prop'; initial?.type === 'CallExpression' &&
} else { initial.callee.type === 'Identifier' &&
binding.initial = initial; 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; 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); const binding = context.state.scope.get(node.name);
// if no binding, means some global variable // if no binding, means some global variable
@ -1502,6 +1520,13 @@ const common_visitors = {
return; 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 * @param {boolean} is_component
*/ */
function validate_slot_attribute(context, attribute, is_component = false) { function validate_slot_attribute(context, attribute, is_component = false) {
const parent = context.path.at(-2);
let owner = undefined; let owner = undefined;
if (parent?.type === 'SnippetBlock') {
if (!is_text_attribute(attribute)) {
e.slot_attribute_invalid(attribute);
}
return;
}
let i = context.path.length; let i = context.path.length;
while (i--) { while (i--) {
const ancestor = context.path[i]; const ancestor = context.path[i];
@ -283,7 +291,7 @@ function validate_slot_attribute(context, attribute, is_component = false) {
owner.type === 'SvelteComponent' || owner.type === 'SvelteComponent' ||
owner.type === 'SvelteSelf' owner.type === 'SvelteSelf'
) { ) {
if (owner !== context.path.at(-2)) { if (owner !== parent) {
e.slot_attribute_invalid_placement(attribute); 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>} * @type {import('zimmerframe').Visitors<import('#compiler').SvelteNode, import('./types.js').AnalysisState>}
*/ */
const validation = { 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) { AssignmentExpression(node, context) {
validate_assignment(node, node.left, context.state); validate_assignment(node, node.left, context.state);
}, },
@ -617,6 +633,11 @@ const validation = {
}); });
}, },
RenderTag(node, context) { 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; context.state.analysis.uses_render_tags = true;
const raw_args = unwrap_optional(node.expression).arguments; const raw_args = unwrap_optional(node.expression).arguments;
@ -626,7 +647,6 @@ const validation = {
} }
} }
const callee = unwrap_optional(node.expression).callee;
if ( if (
callee.type === 'MemberExpression' && callee.type === 'MemberExpression' &&
callee.property.type === 'Identifier' && callee.property.type === 'Identifier' &&
@ -1232,7 +1252,7 @@ export const validation_runes = merge(validation, a11y_validators, {
e.rune_invalid_arguments(node, rune); 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); e.props_invalid_identifier(node);
} }
@ -1240,17 +1260,23 @@ export const validation_runes = merge(validation, a11y_validators, {
e.props_invalid_placement(node); e.props_invalid_placement(node);
} }
for (const property of node.id.properties) { if (node.id.type === 'ObjectPattern') {
if (property.type === 'Property') { for (const property of node.id.properties) {
if (property.computed) { if (property.type === 'Property') {
e.props_invalid_pattern(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 = const value =
property.value.type === 'AssignmentPattern' ? property.value.left : property.value; property.value.type === 'AssignmentPattern' ? property.value.left : property.value;
if (value.type !== 'Identifier') { if (value.type !== 'Identifier') {
e.props_invalid_pattern(property); e.props_invalid_pattern(property);
}
} }
} }
} }

@ -363,7 +363,12 @@ export function client_component(source, analysis, options) {
} }
if (analysis.uses_props || analysis.uses_rest_props) { 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) { if (analysis.custom_element) {
to_remove.push(b.literal('$$host')); to_remove.push(b.literal('$$host'));
} }

@ -1,5 +1,6 @@
import * as b from '../../../utils/builders.js'; import * as b from '../../../utils/builders.js';
import { import {
extract_identifiers,
extract_paths, extract_paths,
is_expression_async, is_expression_async,
is_simple_expression, is_simple_expression,
@ -88,7 +89,7 @@ export function serialize_get_binding(node, state) {
} }
if (binding.kind === 'prop' || binding.kind === 'bindable_prop') { 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); return b.call(node);
} }
@ -119,10 +120,11 @@ export function serialize_get_binding(node, state) {
* @param {import('estree').AssignmentExpression} node * @param {import('estree').AssignmentExpression} node
* @param {import('zimmerframe').Context<import('#compiler').SvelteNode, State>} context * @param {import('zimmerframe').Context<import('#compiler').SvelteNode, State>} context
* @param {() => any} fallback * @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] * @param {{skip_proxy_and_freeze?: boolean}} [options]
* @returns {import('estree').Expression} * @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 { state, visit } = context;
const assignee = node.left; 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 value = path.expression?.(b.id(tmp_id));
const assignment = b.assignment('=', path.node, value); const assignment = b.assignment('=', path.node, value);
original_assignments.push(assignment); 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])) { 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)) 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') { if (binding.kind === 'bindable_prop') {
return b.call( return b.call(
left, left,
b.sequence([ b.assignment(
b.assignment( node.operator,
node.operator, /** @type {import('estree').Pattern} */ (visit(node.left)),
/** @type {import('estree').Pattern} */ (visit(node.left)), value
value ),
), b.true
b.call(left)
])
); );
} else { } else {
return b.call( 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 { } else {
return b.assignment( return b.assignment(
node.operator, node.operator,
@ -525,9 +541,7 @@ function get_hoistable_params(node, context) {
} else if ( } else if (
// If we are referencing a simple $$props value, then we need to reference the object property instead // If we are referencing a simple $$props value, then we need to reference the object property instead
(binding.kind === 'prop' || binding.kind === 'bindable_prop') && (binding.kind === 'prop' || binding.kind === 'bindable_prop') &&
!binding.reassigned && !is_prop_source(binding, context.state)
binding.initial === null &&
!context.state.analysis.accessors
) { ) {
push_unique(b.id('$$props')); push_unique(b.id('$$props'));
} else { } else {
@ -589,7 +603,9 @@ export function get_prop_source(binding, state, name, initial) {
if ( if (
state.analysis.accessors || 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; flags |= PROPS_IS_UPDATED;
} }
@ -624,6 +640,25 @@ export function get_prop_source(binding, state, name, initial) {
return b.call('$.prop', ...args); 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('estree').Expression} node
* @param {import("../../scope.js").Scope | null} scope * @param {import("../../scope.js").Scope | null} scope
@ -672,3 +707,44 @@ export function with_loc(target, source) {
} }
return target; 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') { if (node.name === '$$props') {
return b.id('$$sanitized_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); return serialize_get_binding(node, state);
} }
}, },
@ -98,7 +119,12 @@ export const global_visitors = {
/** @type {import('estree').Pattern} */ (argument), /** @type {import('estree').Pattern} */ (argument),
b.literal(1) 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)); const value = /** @type {import('estree').Expression} */ (visit(argument));
if (serialized_assignment === assignment) { if (serialized_assignment === assignment) {
// No change to output -> nothing to transform -> we can keep the original update expression // 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 * as assert from '../../../../utils/assert.js';
import { import {
get_prop_source, get_prop_source,
is_prop_source,
is_state_source, is_state_source,
serialize_proxy_reassignment, serialize_proxy_reassignment,
should_proxy_or_freeze should_proxy_or_freeze
@ -237,49 +238,65 @@ export const javascript_visitors_runes = {
} }
if (rune === '$props') { if (rune === '$props') {
assert.equal(declarator.id.type, 'ObjectPattern');
/** @type {string[]} */ /** @type {string[]} */
const seen = []; const seen = ['$$slots', '$$events', '$$legacy'];
for (const property of declarator.id.properties) { if (state.analysis.custom_element) {
if (property.type === 'Property') { seen.push('$$host');
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 (binding.reassigned || state.analysis.accessors || initial) { if (declarator.id.type === 'Identifier') {
declarations.push(b.declarator(id, get_prop_source(binding, state, name, initial))); /** @type {import('estree').Expression[]} */
} const args = [b.id('$$props'), b.array(seen.map((name) => b.literal(name)))];
} else {
// RestElement if (state.options.dev) {
/** @type {import('estree').Expression[]} */ // include rest name, so we can provide informative error messages
const args = [b.id('$$props'), b.array(seen.map((name) => b.literal(name)))]; args.push(b.literal(declarator.id.name));
}
if (state.options.dev) {
// include rest name, so we can provide informative error messages declarations.push(b.declarator(declarator.id, b.call('$.rest_props', ...args)));
args.push( } else {
b.literal(/** @type {import('estree').Identifier} */ (property.argument).name) 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, function_visitor,
get_assignment_value, get_assignment_value,
serialize_get_binding, serialize_get_binding,
serialize_set_binding serialize_set_binding,
create_derived,
create_derived_block_argument
} from '../utils.js'; } from '../utils.js';
import { import {
AttributeAliases, AttributeAliases,
@ -34,6 +36,7 @@ import {
EACH_KEYED, EACH_KEYED,
is_capture_event, is_capture_event,
TEMPLATE_FRAGMENT, TEMPLATE_FRAGMENT,
TEMPLATE_UNSET_START,
TEMPLATE_USE_IMPORT_NODE, TEMPLATE_USE_IMPORT_NODE,
TRANSITION_GLOBAL, TRANSITION_GLOBAL,
TRANSITION_IN, 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 {import('#compiler').Component | import('#compiler').SvelteComponent | import('#compiler').SvelteSelf} node
* @param {string} component_name * @param {string} component_name
@ -886,7 +880,12 @@ function serialize_inline_component(node, component_name, context) {
if (slot_name === 'default' && !has_children_prop) { if (slot_name === 'default' && !has_children_prop) {
push_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 // 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 // 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))); push_prop(b.init('$$slots', b.object(serialized_slots)));
} }
if (!context.state.analysis.runes) {
push_prop(b.init('$$legacy', b.true));
}
const props_expression = const props_expression =
props_and_spreads.length === 0 || props_and_spreads.length === 0 ||
(props_and_spreads.length === 1 && Array.isArray(props_and_spreads[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) => { fn = (node_id) => {
return b.call( return b.call(
'$.component', '$.component',
node_id,
b.thunk(/** @type {import('estree').Expression} */ (context.visit(node.expression))), b.thunk(/** @type {import('estree').Expression} */ (context.visit(node.expression))),
b.arrow( b.arrow(
[b.id(component_name)], [b.id(component_name)],
@ -1094,13 +1098,12 @@ function serialize_update(statement) {
} }
/** /**
* * @param {import('estree').Statement[]} update
* @param {import('../types.js').ComponentClientTransformState} state
*/ */
function serialize_render_stmt(state) { function serialize_render_stmt(update) {
return state.update.length === 1 return update.length === 1
? serialize_update(state.update[0]) ? serialize_update(update[0])
: b.stmt(b.call('$.template_effect', b.thunk(b.block(state.update)))); : 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 }); 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] === '<!>'; const use_comment_template = state.template.length === 1 && state.template[0] === '<!>';
if (use_comment_template) { if (use_comment_template) {
// special case — we can use `$.comment` instead of creating a unique 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 { } else {
let flags = TEMPLATE_FRAGMENT; let flags = TEMPLATE_FRAGMENT;
if (unset) {
flags |= TEMPLATE_UNSET_START;
}
if (state.metadata.context.template_needs_import_node) { if (state.metadata.context.template_needs_import_node) {
flags |= TEMPLATE_USE_IMPORT_NODE; flags |= TEMPLATE_USE_IMPORT_NODE;
} }
@ -1707,7 +1731,7 @@ export const template_visitors = {
} }
if (state.update.length > 0) { if (state.update.length > 0) {
body.push(serialize_render_stmt(state)); body.push(serialize_render_stmt(state.update));
} }
body.push(...state.after_update); body.push(...state.after_update);
@ -1830,27 +1854,26 @@ export const template_visitors = {
context.state.template.push('<!>'); context.state.template.push('<!>');
const callee = unwrap_optional(node.expression).callee; const callee = unwrap_optional(node.expression).callee;
const raw_args = unwrap_optional(node.expression).arguments; const raw_args = unwrap_optional(node.expression).arguments;
const is_reactive =
callee.type !== 'Identifier' || context.state.scope.get(callee.name)?.kind !== 'normal';
/** @type {import('estree').Expression[]} */ const args = raw_args.map((arg) =>
const args = [context.state.node]; b.thunk(/** @type {import('estree').Expression} */ (context.visit(arg)))
for (const arg of raw_args) { );
args.push(b.thunk(/** @type {import('estree').Expression} */ (context.visit(arg))));
}
let snippet_function = /** @type {import('estree').Expression} */ (context.visit(callee)); let snippet_function = /** @type {import('estree').Expression} */ (context.visit(callee));
if (context.state.options.dev) { if (context.state.options.dev) {
snippet_function = b.call('$.validate_snippet', snippet_function); snippet_function = b.call('$.validate_snippet', snippet_function);
} }
if (is_reactive) { if (node.metadata.dynamic) {
context.state.init.push(b.stmt(b.call('$.snippet', b.thunk(snippet_function), ...args))); context.state.init.push(
b.stmt(b.call('$.snippet', context.state.node, b.thunk(snippet_function), ...args))
);
} else { } else {
context.state.init.push( context.state.init.push(
b.stmt( b.stmt(
(node.expression.type === 'CallExpression' ? b.call : b.maybe_call)( (node.expression.type === 'CallExpression' ? b.call : b.maybe_call)(
snippet_function, snippet_function,
context.state.node,
...args ...args
) )
) )
@ -1913,7 +1936,7 @@ export const template_visitors = {
} }
if (node.name === 'noscript') { if (node.name === 'noscript') {
context.state.template.push('<!>'); context.state.template.push('<noscript></noscript>');
return; return;
} }
if (node.name === 'script') { if (node.name === 'script') {
@ -1953,6 +1976,7 @@ export const template_visitors = {
let has_content_editable_binding = false; let has_content_editable_binding = false;
let img_might_be_lazy = false; let img_might_be_lazy = false;
let might_need_event_replaying = false; let might_need_event_replaying = false;
let has_direction_attribute = false;
if (is_custom_element) { if (is_custom_element) {
// cloneNode is faster, but it does not instantiate the underlying class of the // 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') { if (node.name === 'img' && attribute.name === 'loading') {
img_might_be_lazy = true; img_might_be_lazy = true;
} }
if (attribute.name === 'dir') {
has_direction_attribute = true;
}
if ( if (
(attribute.name === 'value' || attribute.name === 'checked') && (attribute.name === 'value' || attribute.name === 'checked') &&
!is_text_attribute(attribute) !is_text_attribute(attribute)
@ -2030,7 +2057,7 @@ export const template_visitors = {
} }
if (needs_input_reset && node.name === 'input') { 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') { if (needs_content_reset && node.name === 'textarea') {
@ -2151,8 +2178,15 @@ export const template_visitors = {
state.options.preserveComments 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) { for (const node of hoisted) {
context.visit(node, state); context.visit(node, child_state);
} }
process_children( process_children(
@ -2165,9 +2199,27 @@ export const template_visitors = {
: context.state.node : context.state.node
), ),
true, 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) { if (child_locations.length > 0) {
// @ts-expect-error // @ts-expect-error
location.push(child_locations); location.push(child_locations);
@ -2256,7 +2308,7 @@ export const template_visitors = {
/** @type {import('estree').Statement[]} */ /** @type {import('estree').Statement[]} */
const inner = inner_context.state.init; const inner = inner_context.state.init;
if (inner_context.state.update.length > 0) { 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(...inner_context.state.after_update);
inner.push( inner.push(
@ -2433,7 +2485,7 @@ export const template_visitors = {
: b.id(node.index); : b.id(node.index);
const item = each_node_meta.item; const item = each_node_meta.item;
const binding = /** @type {import('#compiler').Binding} */ (context.state.scope.get(item.name)); const binding = /** @type {import('#compiler').Binding} */ (context.state.scope.get(item.name));
binding.expression = (id) => { binding.expression = (/** @type {import("estree").Identifier} */ id) => {
const item_with_loc = with_loc(item, id); const item_with_loc = with_loc(item, id);
return b.call('$.unwrap', item_with_loc); return b.call('$.unwrap', item_with_loc);
}; };
@ -2587,6 +2639,45 @@ export const template_visitors = {
AwaitBlock(node, context) { AwaitBlock(node, context) {
context.state.template.push('<!>'); 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( context.state.init.push(
b.stmt( b.stmt(
b.call( b.call(
@ -2599,28 +2690,8 @@ export const template_visitors = {
/** @type {import('estree').BlockStatement} */ (context.visit(node.pending)) /** @type {import('estree').BlockStatement} */ (context.visit(node.pending))
) )
: b.literal(null), : b.literal(null),
node.then then_block,
? b.arrow( catch_block
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)
) )
) )
); );
@ -2699,10 +2770,10 @@ export const template_visitors = {
let snippet = b.arrow(args, body); let snippet = b.arrow(args, body);
if (context.state.options.dev) { 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>` // Top-level snippets are hoisted so they can be referenced in the `<script>`
if (context.path.length === 1 && context.path[0].type === 'Fragment') { if (context.path.length === 1 && context.path[0].type === 'Fragment') {
@ -2757,6 +2828,7 @@ export const template_visitors = {
assignment, assignment,
context, context,
() => /** @type {import('estree').Expression} */ (visit(assignment)), () => /** @type {import('estree').Expression} */ (visit(assignment)),
null,
{ {
skip_proxy_and_freeze: true skip_proxy_and_freeze: true
} }
@ -2951,16 +3023,14 @@ export const template_visitors = {
} }
}, },
Component(node, context) { Component(node, context) {
const binding = context.state.scope.get( if (node.metadata.dynamic) {
node.name.includes('.') ? node.name.slice(0, node.name.indexOf('.')) : node.name
);
if (binding !== null && binding.kind !== 'normal') {
// Handle dynamic references to what seems like static inline components // Handle dynamic references to what seems like static inline components
const component = serialize_inline_component(node, '$$component', context); const component = serialize_inline_component(node, '$$component', context);
context.state.init.push( context.state.init.push(
b.stmt( b.stmt(
b.call( b.call(
'$.component', '$.component',
context.state.node,
// TODO use untrack here to not update when binding changes? // 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 // Would align with Svelte 4 behavior, but it's arguably nicer/expected to update this
b.thunk( b.thunk(
@ -2972,6 +3042,7 @@ export const template_visitors = {
); );
return; return;
} }
const component = serialize_inline_component(node, node.name, context); const component = serialize_inline_component(node, node.name, context);
context.state.init.push(component); 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); scopes.set(node.value, value_scope);
context.visit(node.value, { scope: value_scope }); context.visit(node.value, { scope: value_scope });
for (const id of extract_identifiers(node.value)) { 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'); 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); scopes.set(node.error, error_scope);
context.visit(node.error, { scope: error_scope }); context.visit(node.error, { scope: error_scope });
for (const id of extract_identifiers(node.error)) { 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'); 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 * @deprecated Create this utility type yourself instead
*/ */
export interface SveltePreprocessor< 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 * 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 { export interface RenderTag extends BaseNode {
type: 'RenderTag'; type: 'RenderTag';
expression: SimpleCallExpression | (ChainExpression & { expression: SimpleCallExpression }); expression: SimpleCallExpression | (ChainExpression & { expression: SimpleCallExpression });
metadata: {
dynamic: boolean;
};
} }
type Tag = ExpressionTag | HtmlTag | ConstTag | DebugTag | RenderTag; type Tag = ExpressionTag | HtmlTag | ConstTag | DebugTag | RenderTag;
@ -271,6 +274,9 @@ interface BaseElement extends BaseNode {
export interface Component extends BaseElement { export interface Component extends BaseElement {
type: 'Component'; type: 'Component';
metadata: {
dynamic: boolean;
};
} }
interface TitleElement extends BaseElement { interface TitleElement extends BaseElement {

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

@ -189,14 +189,31 @@ export type ComponentEvents<Comp extends SvelteComponent> =
Comp extends SvelteComponent<any, infer Events> ? Events : never; Comp extends SvelteComponent<any, infer Events> ? Events : never;
/** /**
* Convenience type to get the props the given component expects. Example: * Convenience type to get the props the given component expects.
* ```html
* <script lang="ts">
* import type { ComponentProps } from 'svelte';
* import Component from './Component.svelte';
* *
* const props: ComponentProps<Component> = { foo: 'bar' }; // Errors if these aren't the correct props * Example: Ensure a variable contains the props expected by `MyComponent`:
* </script> *
* ```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>> = 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: * 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 * ```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. * 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 // this conditional allows tuples but not arrays. Arrays would indicate a
// rest parameter type, which is not supported. If rest parameters are added // rest parameter type, which is not supported. If rest parameters are added
// in the future, the condition can be removed. // in the future, the condition can be removed.
number extends T['length'] number extends Parameters['length']
? never ? never
: { : {
( (
this: void, this: void,
...args: T ...args: Parameters
): typeof SnippetReturn & { ): typeof SnippetReturn & {
_: 'functions passed to {@render ...} tags must use the `Snippet` type imported from "svelte"'; _: '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; 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 */ /** 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 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_SYMBOL = Symbol('$state');
export const STATE_FROZEN_SYMBOL = Symbol('$state.frozen'); export const STATE_FROZEN_SYMBOL = Symbol('$state.frozen');

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

@ -1,20 +1,7 @@
import { DEV } from 'esm-env';
import { snapshot } from '../proxy.js'; import { snapshot } from '../proxy.js';
import { render_effect, validate_effect } from '../reactivity/effects.js'; import { inspect_effect, validate_effect } from '../reactivity/effects.js';
import { deep_read, untrack } from '../runtime.js';
import { array_prototype, get_prototype_of, object_prototype } from '../utils.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 {() => any[]} get_value
* @param {Function} [inspector] * @param {Function} [inspector]
@ -25,32 +12,9 @@ export function inspect(get_value, inspector = console.log) {
let initial = true; let initial = true;
// we assign the function directly to signals, rather than just inspect_effect(() => {
// calling `inspector` directly inside the effect, so that inspector(initial ? 'init' : 'update', ...deep_snapshot(get_value()));
// we get useful stack traces initial = false;
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);
}
};
}); });
} }

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

@ -24,12 +24,14 @@ import {
run_out_transitions, run_out_transitions,
pause_children, pause_children,
pause_effect, pause_effect,
resume_effect resume_effect,
get_first_node
} from '../../reactivity/effects.js'; } from '../../reactivity/effects.js';
import { source, mutable_source, set } from '../../reactivity/sources.js'; import { source, mutable_source, set } from '../../reactivity/sources.js';
import { is_array, is_frozen } from '../../utils.js'; import { is_array, is_frozen } from '../../utils.js';
import { INERT, STATE_SYMBOL } from '../../constants.js'; import { INERT, STATE_SYMBOL } from '../../constants.js';
import { queue_micro_task } from '../task.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 * 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 * Pause multiple effects simultaneously, and coordinate their
* subsequent destruction. Used in each blocks * subsequent destruction. Used in each blocks
* @param {import('#client').EachState} state
* @param {import('#client').EachItem[]} items * @param {import('#client').EachItem[]} items
* @param {null | Node} controlled_anchor * @param {null | Node} controlled_anchor
* @param {Map<any, import("#client").EachItem>} items_map * @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[]} */ /** @type {import('#client').TransitionManager[]} */
var transitions = []; var transitions = [];
var length = items.length; var length = items.length;
@ -77,7 +80,7 @@ function pause_effects(items, controlled_anchor, items_map) {
clear_text_content(parent_node); clear_text_content(parent_node);
parent_node.append(/** @type {Element} */ (controlled_anchor)); parent_node.append(/** @type {Element} */ (controlled_anchor));
items_map.clear(); items_map.clear();
link(items[0].prev, items[length - 1].next); link(state, items[0].prev, items[length - 1].next);
} }
run_out_transitions(transitions, () => { run_out_transitions(transitions, () => {
@ -85,8 +88,7 @@ function pause_effects(items, controlled_anchor, items_map) {
var item = items[i]; var item = items[i];
if (!is_controlled) { if (!is_controlled) {
items_map.delete(item.k); items_map.delete(item.k);
item.o.remove(); link(state, item.prev, item.next);
link(item.prev, item.next);
} }
destroy_effect(item.e, !is_controlled); 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) { export function each(anchor, flags, get_collection, get_key, render_fn, fallback_fn = null) {
/** @type {import('#client').EachState} */ /** @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; 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} */ /** @type {import('#client').Effect | null} */
var fallback = null; var fallback = null;
block(() => { block(anchor, 0, () => {
var collection = get_collection(); var collection = get_collection();
var array = is_array(collection) var array = is_array(collection)
@ -151,7 +153,7 @@ export function each(anchor, flags, get_collection, get_key, render_fn, fallback
if (hydrating) { if (hydrating) {
var is_else = /** @type {Comment} */ (anchor).data === HYDRATION_END_ELSE; 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 // hydration mismatch — remove the server-rendered DOM and start over
remove(hydrate_nodes); remove(hydrate_nodes);
set_hydrating(false); set_hydrating(false);
@ -164,8 +166,8 @@ export function each(anchor, flags, get_collection, get_key, render_fn, fallback
/** @type {Node} */ /** @type {Node} */
var child_anchor = hydrate_start; var child_anchor = hydrate_start;
/** @type {import('#client').EachItem | import('#client').EachState} */ /** @type {import('#client').EachItem | null} */
var prev = state; var prev = null;
/** @type {import('#client').EachItem} */ /** @type {import('#client').EachItem} */
var item; var item;
@ -182,11 +184,10 @@ export function each(anchor, flags, get_collection, get_key, render_fn, fallback
break; break;
} }
var child_open = /** @type {Comment} */ (child_anchor);
child_anchor = hydrate_anchor(child_anchor); child_anchor = hydrate_anchor(child_anchor);
var value = array[i]; var value = array[i];
var key = get_key(value, 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); state.items.set(key, item);
child_anchor = /** @type {Comment} */ (child_anchor.nextSibling); 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 length = array.length;
var items = state.items; var items = state.items;
var first = state.next; var first = state.first;
var current = first; var current = first;
/** @type {Set<import('#client').EachItem>} */ /** @type {Set<import('#client').EachItem>} */
var seen = new Set(); var seen = new Set();
/** @type {import('#client').EachState | import('#client').EachItem} */ /** @type {import('#client').EachItem | null} */
var prev = state; var prev = null;
/** @type {Set<import('#client').EachItem>} */ /** @type {Set<import('#client').EachItem>} */
var to_animate = new Set(); var to_animate = new Set();
@ -293,16 +294,13 @@ function reconcile(array, state, anchor, render_fn, flags, get_key) {
item = items.get(key); item = items.get(key);
if (item === undefined) { if (item === undefined) {
var child_open = empty(); var child_anchor = current ? get_first_node(current.e) : anchor;
var child_anchor = current ? current.o : anchor;
child_anchor.before(child_open);
prev = create_item( prev = create_item(
child_open,
child_anchor, child_anchor,
state,
prev, prev,
prev.next, prev === null ? state.first : prev.next,
value, value,
key, key,
i, i,
@ -351,9 +349,9 @@ function reconcile(array, state, anchor, render_fn, flags, get_key) {
seen.delete(stashed[j]); seen.delete(stashed[j]);
} }
link(a.prev, b.next); link(state, a.prev, b.next);
link(prev, a); link(state, prev, a);
link(b, start); link(state, b, start);
current = start; current = start;
prev = b; prev = b;
@ -366,9 +364,9 @@ function reconcile(array, state, anchor, render_fn, flags, get_key) {
seen.delete(item); seen.delete(item);
move(item, current, anchor); move(item, current, anchor);
link(item.prev, item.next); link(state, item.prev, item.next);
link(item, prev.next); link(state, item, prev === null ? state.first : prev.next);
link(prev, item); link(state, prev, item);
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) { 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 * @template V
* @param {Comment | Text} open
* @param {Node} anchor * @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 {import('#client').EachItem | null} next
* @param {V} value * @param {V} value
* @param {unknown} key * @param {unknown} key
@ -462,7 +463,7 @@ function update_item(item, value, index, type) {
* @param {number} flags * @param {number} flags
* @returns {import('#client').EachItem} * @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; var previous_each_item = current_each_item;
try { try {
@ -480,16 +481,27 @@ function create_item(open, anchor, prev, next, value, key, index, render_fn, fla
a: null, a: null,
// @ts-expect-error // @ts-expect-error
e: null, e: null,
o: open,
prev, prev,
next next
}; };
prev.next = item;
if (next !== null) next.prev = item;
current_each_item = 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; return item;
} finally { } finally {
@ -503,10 +515,10 @@ function create_item(open, anchor, prev, next, value, key, index, render_fn, fla
* @param {Text | Element | Comment} anchor * @param {Text | Element | Comment} anchor
*/ */
function move(item, next, anchor) { function move(item, next, anchor) {
var end = item.next ? item.next.o : anchor; var end = item.next ? get_first_node(item.next.e) : anchor;
var dest = next ? next.o : 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) { while (node !== end) {
var next_node = /** @type {import('#client').TemplateNode} */ (node.nextSibling); var next_node = /** @type {import('#client').TemplateNode} */ (node.nextSibling);
@ -516,11 +528,20 @@ function move(item, next, anchor) {
} }
/** /**
* * @param {import('#client').EachState} state
* @param {import('#client').EachItem | import('#client').EachState} prev * @param {import('#client').EachItem | null} prev
* @param {import('#client').EachItem | null} next * @param {import('#client').EachItem | null} next
*/ */
function link(prev, next) { function link(state, prev, next) {
prev.next = next; if (prev === null) {
if (next !== null) next.prev = prev; 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 { block, branch, destroy_effect } from '../../reactivity/effects.js';
import { render_effect } from '../../reactivity/effects.js'; import { get_start, hydrate_nodes, hydrating } from '../hydration.js';
import { current_effect, get } from '../../runtime.js'; import { create_fragment_from_html } from '../reconciler.js';
import { is_array } from '../../utils.js'; import { assign_nodes } from '../template.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;
}
}
/** /**
* @param {Element | Text | Comment} anchor * @param {Element | Text | Comment} anchor
@ -34,72 +11,52 @@ function remove_from_parent_effect(effect, to_remove) {
* @returns {void} * @returns {void}
*/ */
export function html(anchor, get_value, svg, mathml) { export function html(anchor, get_value, svg, mathml) {
const parent_effect = anchor.parentNode !== current_effect?.dom ? current_effect : null; var value = '';
let value = derived(get_value);
render_effect(() => { /** @type {import('#client').Effect | null} */
var dom = html_to_dom(anchor, parent_effect, get(value), svg, mathml); var effect;
if (dom) { block(anchor, 0, () => {
return () => { if (value === (value = get_value())) return;
if (parent_effect !== null) {
remove_from_parent_effect(parent_effect, is_array(dom) ? dom : [dom]);
}
remove(dom);
};
}
});
}
/** if (effect) {
* Creates the content for a `@html` tag from its string value, destroy_effect(effect);
* inserts it before the target anchor and returns the new nodes. effect = null;
* @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>`;
// Don't use create_fragment_with_script_from_html here because that would mean script tags are executed. if (value === '') return;
// @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) { effect = branch(() => {
node = /** @type {Element} */ (node.firstChild); if (hydrating) {
} assign_nodes(get_start(), hydrate_nodes[hydrate_nodes.length - 1]);
return;
}
if (node.childNodes.length === 1) { var html = value + '';
var child = /** @type {Text | Element | Comment} */ (node.firstChild); if (svg) html = `<svg>${html}</svg>`;
target.before(child); else if (mathml) html = `<math>${html}</math>`;
if (effect !== null) {
push_template_node(child, effect);
}
return child;
}
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) { if (svg || mathml) {
while (node.firstChild) { node = /** @type {Element} */ (node.firstChild);
target.before(node.firstChild); }
}
} else {
target.before(node);
}
if (effect !== null) { assign_nodes(
push_template_node(nodes, effect); /** @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; var flags = elseif ? EFFECT_TRANSPARENT : 0;
block(() => { block(anchor, flags, () => {
if (condition === (condition = !!get_condition())) return; if (condition === (condition = !!get_condition())) return;
/** Whether or not there was a hydration mismatch. Needs to be a `let` or else it isn't treeshaken out */ /** Whether or not there was a hydration mismatch. Needs to be a `let` or else it isn't treeshaken out */
@ -78,5 +78,5 @@ export function if_block(
// continue in hydration mode // continue in hydration mode
set_hydrating(true); set_hydrating(true);
} }
}, flags); });
} }

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

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

@ -1,22 +1,21 @@
import { block, branch, pause_effect } from '../../reactivity/effects.js'; import { block, branch, pause_effect } from '../../reactivity/effects.js';
// TODO seems weird that `anchor` is unused here — possible bug?
/** /**
* @template P * @template P
* @template {(props: P) => void} C * @template {(props: P) => void} C
* @param {import('#client').TemplateNode} anchor
* @param {() => C} get_component * @param {() => C} get_component
* @param {(component: C) => import('#client').Dom | void} render_fn * @param {(component: C) => import('#client').Dom | void} render_fn
* @returns {void} * @returns {void}
*/ */
export function component(get_component, render_fn) { export function component(anchor, get_component, render_fn) {
/** @type {C} */ /** @type {C} */
let component; let component;
/** @type {import('#client').Effect | null} */ /** @type {import('#client').Effect | null} */
let effect; let effect;
block(() => { block(anchor, 0, () => {
if (component === (component = get_component())) return; if (component === (component = get_component())) return;
if (effect) { 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_each_item, set_current_each_item } from './each.js';
import { current_component_context, current_effect } from '../../runtime.js'; import { current_component_context, current_effect } from '../../runtime.js';
import { DEV } from 'esm-env'; import { DEV } from 'esm-env';
import { is_array } from '../../utils.js'; import { assign_nodes } from '../template.js';
import { push_template_node } from '../template.js';
import { noop } from '../../../shared/utils.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 {Comment | Element} node
* @param {() => string} get_tag * @param {() => string} get_tag
@ -47,7 +25,6 @@ function swap_block_dom(effect, from, to) {
* @returns {void} * @returns {void}
*/ */
export function element(node, get_tag, is_svg, render_fn, get_namespace, location) { 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; const filename = DEV && location && current_component_context?.function.filename;
/** @type {string | null} */ /** @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; let each_item_block = current_each_item;
block(() => { block(anchor, 0, () => {
const next_tag = get_tag() || null; const next_tag = get_tag() || null;
const ns = get_namespace const ns = get_namespace
? 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.createElementNS(ns, next_tag)
: document.createElement(next_tag); : document.createElement(next_tag);
assign_nodes(element, element);
if (DEV && location) { if (DEV && location) {
// @ts-expect-error // @ts-expect-error
element.__svelte_meta = { 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 (render_fn) {
// If hydrating, use the existing ssr comment as the anchor so that the // 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 // 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); 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 // See below
return noop; return noop;
}); });

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

@ -1,6 +1,6 @@
import { DEV } from 'esm-env'; import { DEV } from 'esm-env';
import { hydrating } from '../hydration.js'; 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 { import {
AttributeAliases, AttributeAliases,
DelegatedEvents, 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 * 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. * to remove it upon hydration to avoid a bug when someone resets the form value.
* @param {HTMLInputElement} dom * @param {HTMLInputElement} input
* @returns {void} * @returns {void}
*/ */
export function remove_input_attr_defaults(dom) { export function remove_input_defaults(input) {
if (hydrating) { if (!hydrating) return;
let already_removed = false;
// We try and remove the default attributes later, rather than sync during hydration. var already_removed = false;
// 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 // We try and remove the default attributes later, rather than sync during hydration.
// the idle callback, then we ensure the input defaults are cleared just before. // Doing it sync during hydration has a negative impact on performance, but deferring the
const remove_defaults = () => { // work in an idle task alleviates this greatly. If a form reset event comes in before
if (already_removed) return; // the idle callback, then we ensure the input defaults are cleared just before.
already_removed = true; var remove_defaults = () => {
const value = dom.getAttribute('value'); if (already_removed) return;
set_attribute(dom, 'value', null); already_removed = true;
set_attribute(dom, 'checked', null);
if (value) dom.value = value; // Remove the attributes but preserve the values
}; if (input.hasAttribute('value')) {
// @ts-expect-error var value = input.value;
dom.__on_r = remove_defaults; set_attribute(input, 'value', null);
queue_idle_task(remove_defaults); input.value = value;
add_form_reset_listener(); }
}
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 = ''; next.class = '';
} }
var setters = map_get(setters_cache, element.nodeName); var setters = setters_cache.get(element.nodeName);
if (!setters) map_set(setters_cache, element.nodeName, (setters = get_setters(element))); if (!setters) setters_cache.set(element.nodeName, (setters = get_setters(element)));
// @ts-expect-error // @ts-expect-error
var attributes = /** @type {Record<string, unknown>} **/ (element.__attributes ??= {}); 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) { export function bind_current_time(media, get_value, update) {
/** @type {number} */ /** @type {number} */
var raf_id; 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 // 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 // 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); raf_id = requestAnimationFrame(callback);
} }
updating = true; var next_value = media.currentTime;
update(media.currentTime); if (value !== next_value) {
update((value = next_value));
}
}; };
raf_id = requestAnimationFrame(callback); raf_id = requestAnimationFrame(callback);
media.addEventListener('timeupdate', callback); media.addEventListener('timeupdate', callback);
render_effect(() => { render_effect(() => {
var value = get_value(); var next_value = Number(get_value());
// through isNaN we also allow number strings, which is more robust if (value !== next_value && !isNaN(/** @type {any} */ (next_value))) {
if (!updating && !isNaN(/** @type {any} */ (value))) { media.currentTime = value = next_value;
media.currentTime = /** @type {number} */ (value);
} }
updating = false;
}); });
teardown(() => cancelAnimationFrame(raf_id)); teardown(() => cancelAnimationFrame(raf_id));
@ -113,22 +113,21 @@ export function bind_ready_state(media, update) {
* @param {(playback_rate: number) => void} update * @param {(playback_rate: number) => void} update
*/ */
export function bind_playback_rate(media, get_value, update) { export function bind_playback_rate(media, get_value, update) {
var updating = false; // 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
// Needs to happen after the element is inserted into the dom, else playback will be set back to 1 by the browser.
// For hydration we could do it immediately but the additional code is not worth the lost microtask.
effect(() => { effect(() => {
var value = get_value(); var value = Number(get_value());
// through isNaN we also allow number strings, which is more robust if (value !== media.playbackRate && !isNaN(value)) {
if (!isNaN(/** @type {any} */ (value)) && value !== media.playbackRate) { media.playbackRate = value;
updating = true;
media.playbackRate = /** @type {number} */ (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'], () => { listen(media, ['ratechange'], () => {
if (!updating) update(media.playbackRate); update(media.playbackRate);
updating = false;
}); });
}); });
} }
@ -200,9 +199,7 @@ export function bind_paused(media, get_value, update) {
* @param {(volume: number) => void} update * @param {(volume: number) => void} update
*/ */
export function bind_volume(media, get_value, update) { export function bind_volume(media, get_value, update) {
var updating = false;
var callback = () => { var callback = () => {
updating = true;
update(media.volume); update(media.volume);
}; };
@ -213,14 +210,11 @@ export function bind_volume(media, get_value, update) {
listen(media, ['volumechange'], callback, false); listen(media, ['volumechange'], callback, false);
render_effect(() => { render_effect(() => {
var value = get_value(); var value = Number(get_value());
// through isNaN we also allow number strings, which is more robust if (value !== media.volume && !isNaN(value)) {
if (!updating && !isNaN(/** @type {any} */ (value))) { media.volume = value;
media.volume = /** @type {number} */ (value);
} }
updating = false;
}); });
} }
@ -230,10 +224,7 @@ export function bind_volume(media, get_value, update) {
* @param {(muted: boolean) => void} update * @param {(muted: boolean) => void} update
*/ */
export function bind_muted(media, get_value, update) { export function bind_muted(media, get_value, update) {
var updating = false;
var callback = () => { var callback = () => {
updating = true;
update(media.muted); update(media.muted);
}; };
@ -244,9 +235,8 @@ export function bind_muted(media, get_value, update) {
listen(media, ['volumechange'], callback, false); listen(media, ['volumechange'], callback, false);
render_effect(() => { render_effect(() => {
var value = get_value(); var value = !!get_value();
if (!updating) media.muted = !!value; if (media.muted !== value) media.muted = value;
updating = false;
}); });
} }

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

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

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

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

@ -30,6 +30,17 @@ export function set_hydrate_nodes(nodes) {
hydrate_start = nodes && nodes[0]; 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 * 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` * 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 // text node to hydrate — we must therefore create one
if (is_text && hydrate_start?.nodeType !== 3) { if (is_text && hydrate_start?.nodeType !== 3) {
var text = empty(); var text = empty();
var dom = /** @type {import('#client').TemplateNode[]} */ ( var effect = /** @type {import('#client').Effect} */ (current_effect);
/** @type {import('#client').Effect} */ (current_effect).dom
);
dom.unshift(text); if (effect.nodes?.start === hydrate_start) {
hydrate_start?.before(text); effect.nodes.start = text;
}
hydrate_start?.before(text);
return text; return text;
} }
@ -122,13 +122,7 @@ export function sibling(node, is_text = false) {
// text node to hydrate — we must therefore create one // text node to hydrate — we must therefore create one
if (is_text && type !== 3) { if (is_text && type !== 3) {
var text = empty(); var text = empty();
var dom = /** @type {import('#client').TemplateNode[]} */ (
/** @type {import('#client').Effect} */ (current_effect).dom
);
dom.unshift(text);
next_sibling?.before(text); next_sibling?.before(text);
return text; return text;
} }

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

Loading…
Cancel
Save